问题描述
以前做过一次SpringBoot整合Security的项目,但最近重新尝试SpringBoot整合Security的项目时却碰到了问题
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
原来SpringBoot升级到了2.0之后的版本,Security也由原来的版本4升级到了5,所以花了点时间研究,以此记录
工具
- IDE:IDEA
- 项目依赖管理:Maven
- 持久层框架:JPA
SpringBoot 2.0 整合 Security5
1. 项目主要依赖
security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SpringBoot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
同时引入SpringData JPA持久层框架和 Lombok工具简化GET、SET方法
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. SpringBoot配置
这里我使用更加简洁的yml文件来配置
properties.yml:
server:
port: 8888
spring:
datasource:
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/three_point
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
ddl-auto: update
:使用了JPA的自动建表,根据实体类自动生成数据表(需要先创建数据库)
3. 实体类
用户类
package com.pan.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.List;
@Entity
@Table
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<Role> roles;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
角色类
package com.pan.entity;
import lombok.Data;
import javax.persistence.*;
@Entity
@Table
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
}
4. DAO类
JPA操作接口
package com.pan.repository;
import com.pan.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
UserDetailsService 实现类,用于Security查询角色进行认证
package com.pan.service.impl;
import com.pan.entity.Role;
import com.pan.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
com.pan.entity.User user = userRepository.findByUsername(s);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
这里我使用的是User不继承UserDetails这个接口,而将添加认证用户的操作放在了上面这个类中,还有另外一种实现方式
注:这里为另外一个项目,属性会有所不同,仅供演示,代码如下:
用户类
@Entity
@Table(name = "user")
@Getter
@Setter
public class Member implements UserDetails {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<Role> roles = this.getRoles();
for (Role role : roles) {
auths.add(new SimpleGrantedAuthority(role.getMark()));
}
return auths;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService实现类
public class MemberService implements UserDetailsService {
@Autowired
MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String name) {
Member member = memberRepository.findByUsername(name);
if (member == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return member;
}
}
两种写法都可,但个人认为这种写法较为繁琐
继续原来的内容
5. Controller控制器和HTML视图
控制器
两个简单的请求,登录和注册
package com.pan.controller;
import com.pan.entity.User;
import com.pan.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
public class IndexController {
@Autowired
private UserRepository userRepository;
@ResponseBody
@RequestMapping("/personal_center")
public void login(HttpServletRequest request) {
System.out.println("登录成功");
}
@ResponseBody
@PostMapping("/registry")
public void registry(User user) {
userRepository.save(new User(user.getUsername(), user.getPassword()));
}
}
视图
登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
姓名:<input type="text" name="username">
密码:<input type="password" name="password">
<button type="submit">登录</button>
</form>
</body>
</html>
注册页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/registry" method="post">
姓名:<input type="text" name="username">
密码:<input type="password" name="password">
<button type="submit">注册</button>
</form>
</body>
</html>
界面简陋请见谅
6. Security配置类
视图类配置
package com.pan.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/sign_in").setViewName("login");
registry.addViewController("/sign_up").setViewName("registry");
}
}
响应指定请求,返回HTML视图,简化Controller的书写,等同于以下内容:
/**
* WebMvcConfig类等效内容
*/
@RequestMapping("/sign_in")
public String sign_in() {
return "login";
}
@RequestMapping("/sign_up")
public String sign_up() {
return "registry";
}
Security核心配置类
在SpringBoot 2.0 版本以前,是这样配置的
旧配置
package com.pan.security;
import com.pan.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService detailsService() {
return new UserDetailsServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService());
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.and().authorizeRequests()
.antMatchers("/registry").permitAll()
.anyRequest().authenticated()
.and().formLogin().loginPage("/sign_in")
.loginProcessingUrl("/login").defaultSuccessUrl("/personal_center",true)
.failureUrl("/sign_in?error").permitAll()
.and().sessionManagement().invalidSessionUrl("/sign_in")
.and().rememberMe().tokenValiditySeconds(1209600)
.and().logout().logoutSuccessUrl("/sign_in").permitAll()
.and().csrf().disable();
}
}
具体的配置请自行网上搜索
在数据库中预先存储用户名和密码,然后登录验证,完全没有问题
但现在,却出问题了,后台报错
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
这是什么鬼?以前可没有这种问题啊
查阅资料得知,SpringBoot2.0抛弃了原来的NoOpPasswordEncoder,要求用户保存的密码必须要使用加密算法后存储,在登录验证的时候Security会将获得的密码在进行编码后再和数据库中加密后的密码进行对比,文献如下:
在官方文档中,给出了解决方案,我们可以通过在配置类中添加如下配置来回到原来的写法
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
这样能解决办法,但NoOpPasswordEncoder 已经被官方废弃了,既然废弃它肯定是有原因的,而且这种勉强的做法也不符合我们程序员精益求精的风格
正确做法如下:
写法一:
第一步: 在WebSecurityConfig中定义一个新的bean对象
@Bean
public PasswordEncoder passwordEncoder() {
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
return new BCryptPasswordEncoder();
}
这里使用的是BCryptPasswordEncoder编码方式也可选择其他,如下:
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
PasswordEncoderFactories.createDelegatingPasswordEncoder()方法默认返回值为BCryptPasswordEncoder(),两个return等价
第二步:修改WebSecurityConfig的一个重写方法
SpringBoot2.0以前旧配置为:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService());
}
修改为新配置:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService()).passwordEncoder(passwordEncoder());
}
passwordEncoder(passwordEncoder())
里面的passwordEncoder()
为我们定义的bean
这样在登录的时候就会使用我们选择编码方式进行验证
也可以不写上述configure(AuthenticationManagerBuilder auth)
方法,但需要在Security配置类(WebSecurityConfig)中添加以下内容:
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(detailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
将我们定义的PasswordEncoder的Bean
和自定义的UserDetailsService
注入到DaoAuthenticationProvider
,上面所写的修改方法在Security内部也是创建DaoAuthenticationProvider
,两者等价
由于使用了编码验证,所以我们需要一组编码后的密码,否则会有如下警告:
o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
第三步:修改IndexController
-
自动注入一个PasswordEncoder
@Autowired private PasswordEncoder passwordEncoder;
-
修改
registry()
方法@ResponseBody @PostMapping("/registry") public void registry(User user) { userRepository.save(new User(user.getUsername(), passwordEncoder.encode(user.getPassword()))); }
passwordEncoder.encode(user.getPassword())
在密码保存时进行密码编码加密
也可以将加密封装成一个工具类,方便调用。切记封装工具类要用构造方法生成PasswordEncoder 对象,否则会报空指针异常
工具类封装
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class PasswordEncoderUtil {
private static PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); //使用构造方法生成对象
/**
* 对密码使用BCryptPasswordEncoder加密方式进行加密
*/
public static String passwordEncoder(String password) {
return passwordEncoder.encode(password);
}
}
加密后的密码:
这样就可以完美解决问题了,密码的安全性也有了保障
写法二:
第一步:WebSecurityConfig配置
不定义PasswordEncoder的bean
修改userDetailsService
的configure()
重写方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService()).passwordEncoder(new BCryptPasswordEncoder());
}
若不写该方法,则添加:
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(detailsService());
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
return authenticationProvider;
}
第二步:修改 IndexController
不用自动注入,new一个编码对象,然后使用
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
总结
到此,整合就初步完成了,整合问题也得到了完美解决,希望我的文章能对你有所帮助
源码
源码放在GitHub上了。若有帮助,希望GitHub能给我一个Star,诚挚的感谢!
SecurityDemo