自定义用户认证逻辑
- 处理用户信息获取逻辑
- 处理用户校验逻辑
- 处理密码加密解密
处理用户信息获取逻辑
用户信息的获取逻辑被spring security封装在一个名为UserDetailService接口内部:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
传入用户名后返回UserDetails对象,里面包含用户的具体信息,当用户名不存在的时候抛出异常UsernameNotFoundException,通过实现这个方法来读取用户信息。
下面我们设计一下这个接口的实现类:
@Component
public class MyUserDetailsService implements UserDetailsService{
private Logger logger =LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户名:"+username);
//根据用户名查找用户信息
return new User(username,"123456",AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
上述代码没有从数据库取用户信息,仅为了测试,直接new一个用户,User类是SpringSecurity提供的一个对象,它实现了UserDetails接口,构造函数中的三个参数为用户名,密码和用户权限,此处设置为admin。
实现MyUserDetailsService这个类后,我们重新使用表单登录发现,用户名可以随便填,密码只能设置为“123456”,如果密码出错了,会重定向到页面如下:
当我们输入正确密码123456后,程序才能执行到restful api的方法中,这说明上面这个实现UserDetailsService接口来实现自定义登录的功能实现了。
处理用户校验逻辑
UserDetails接口中有几个要实现的方法。
public interface UserDetails extends Serializable {
//获取权限信息
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//下面的这四个返回boolean值的方法就行实现自己的校验逻辑的地方
//判断账号是否过期
boolean isAccountNonExpired();
//判断账号是否被锁定
boolean isAccountNonLocked();
//判断密码是否过期
boolean isCredentialsNonExpired();
//判断账号是否可用(比如是否被删除)
boolean isEnabled();
}
了解了这个接口的几个函数后,我们可以对上面loadUserByUsername方法中的返回对象User的构造函数进行设置账号是否过期,是否被锁定,密码是否过期以及账号是否可用这四个校验。修改后的代码如下:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户名:"+username);
//根据用户名查找用户信息
return new User(username, "123456", true, true, true, false, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
修改后的方法明显可见,我们使用SpringSecurity中UserDetails接口的实现类User的构造函数有所变化,多了前面四个参数就是我们想要设置的账号是否过期,是否被冻结,密码是否过期以及账号是否可用这四个校验,此处我把第四个参数账号是否被锁定设置为false,所以当我启动应用,在登录表单中输入正确的账号和密码后仍然提示如下:
处理密码加密解密
在实际应用中,保存到数据库中的密码是经过加密的,而不是123456这种明文,所以我们从数据库获取到的密码需要先进行解密。SpringSecurity中进行密码的加密和解密的接口是PasswordEncoder。这个接口如下:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
这个接口的encode方法是对原始密码进行加密,matches方法是判断原始密码和加密后的密码是否匹配。这两个方法被调用的时机不同,encode方法是我们在保存到数据库之前进行实现的对密码进行加密,matches是我们实现后在SpringSecurity在发起登录请求后判断用户填写的原始密码是否和数据库中同一用户名对应的加密密码是否匹配的。
此处我们使用SpringSecurity中PasswordEncoder的实现类BCryptPasswordEncoder来进行加密解密,为此我们先修改一下配置,引入进行进行加密解密的类:
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
修改后启动应用后,输入正确的账号密码登陆页面报错如下:
这时因为我们之前在
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户名:"+username);
//根据用户名查找用户信息
return new User(username, "123456", true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
这个方法中传入的密码仍然是明文,而自动配置类加上PasswordEncoder实现类后,只能传入加密后的密码作为参数,为此我们要修改上面这个loadUserByUsername方法:
@Component
public class MyUserDetailsService implements UserDetailsService{
private Logger logger =LoggerFactory.getLogger(getClass());
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户名:"+username);
//根据用户名查找用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户名:"+username);
//根据用户名查找用户信息
String password =passwordEncoder.encode("123456");
System.out.println("数据库密码是:"+password);
return new User(username, passwordEncoder.encode("123456"), true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
}
通过我们自动注入之前配置好的PasswordEncoder实现类,并且使用它的encode方法对字符串“123456”进行加密后传入,这样启动后输入正确的账号密码后登录就成功了,并且日志显示对密码123456进行加密后的值为:
数据库密码是:$2a$10$km0CLfSoIhsxrKi7TiMHv.PIwx0r.RCZuz1S0ua/Ri7wYMSk1WWeW
并且如果我下回登陆,这个加密后的密码是会变的,这也是我们这次使用SpringSecurity中的BCryptPasswordEncoder这个编码器类的优点,降低了密码被破解的可能性,提高了安全性。
总结
本次讲的这三点分别对应三个要实现的接口,具体如下:
- 处理用户信息获取逻辑 UserDetailsService
- 处理用户校验逻辑 UserDetails
- 处理密码加密解密 PasswordEncoder