引言
在上一篇博客《Spring Security之基本原理》我们用最简单的方式了解了Spring Security,但是我们发现登录只能使用user这个用户,而且密码也只能使用SpringBoot启动的时候生成的密码,在实际中我们的用户应该是从数据库中读取的,因此就要引入自定义用户认证逻辑。
自定义认证逻辑
自定义认证逻辑主要包括三个方面
- 处理用户信息获取逻辑
- 处理用户校验逻辑
- 处理密码加密解密
处理用户信息获取逻辑
在Spring Security里面用户信息的获取逻辑是封装在UserDetailsService这个接口里面的。
我们只需要重写这个接口,把我们获取用户的逻辑写在loadUserByUsername这个方法中就行,这个方法有一个入参就是用户输入的用户名,抛出的异常只有一个用户不存在的异常,这个方法就是根据用户登陆时输入的用户名到你的存储里去读取用户信息,而用户信息被封装在UserDetails接口的某个实现类中,当你返回一个用户信息后,Security就会拿着这个用户信息去做相应的处理和校验。
实现UserDetailService接口
@Component
public class MyUserDetailsService implements UserDetailsService {
//根据自己使用的ORM,可以注入dao或mapper对象到数据库里查出用户信息
//这里为了简化demo演示,就先不涉及数据库的内容了。
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查找用户信息
logger.info("登陆用户名" + username);
return new User(username,"12345", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
这里使用的Usre对象是Spring Security提供的已经实现了UserTails接口的一个类,所以这里直接new了一个Usre对象返回,该构造函数的前俩个参数很好理解,分别是用户名和密码,第三个参数则是一个用户权限的集合,也就是告诉Spring Security,当前返回的用户所拥有的权限。
验证
此时再访问/hello,登陆的用户就可以是任意用户了,因为loadUserByUsername方法是根据用户输入的用户名份到存储里去查找相应的用户,再将密码做一个Match。
此时可以看到,用户名不再是默认的user了,而是用户输入的任意的用户名。
处理用户校验逻辑
主要关注的有俩个方面
- 密码是否匹配
不过这个是由SpringSecurity来完成的,只需要告诉它我们从数据库里读出来的密码是什么就行了。 - 其他方面的校验,如用户是否被冻结,密码是否过期等等。
我们先来看一下上面提到过的UserDetails接口
public interface UserDetails extends Serializable {
// ~ Methods
// ========================================================================================================
/**
* 返回用户的权限信息. 不能返回nul;.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 返回用户用于认证的密码
*/
String getPassword();
/**
*返回用户用于认证的用户名,不能为null
*/
String getUsername();
/**
* 用户判断账户是否过期
*/
boolean isAccountNonExpired();
/**
* 判断用户账户是否被锁定
*/
boolean isAccountNonLocked();
/**
* 判断用户账户的密码是否过期
*/
boolean isCredentialsNonExpired();
/**
* 用户账户是否可用(如果账户被删除了就无法使用了)
*/
boolean isEnabled();
}
再回到我们直接返回的User对象上。查看源码可以看到,该类是提供了两个构造函数的.
显然,前者调用了后者,当我们不指定那个四个布尔值的时候,这四个值默认是为true的。
根据这四个布尔值,可以对用户实现一个不同的认证。
处理密码加密和解密
在上述的演示中,我们之前返回的密码都是明文,在实际中,我们从数据库中拿到的密码应该是加密过的密码。所以为了解决这个问题,我们要介绍PasswordEncoder这个接口。
public interface PasswordEncoder {
/**
* 对用户密码进行加加密
* 由我们来调用,在用户注册完之后将密码加密后存储到数据库中
*/
String encode(CharSequence rawPassword);
/**
* 判断加密以后的密码与用户输入的密码是否匹配
* Security调用的,无需我们关心
* Security拿到返回的UserDetails后,会将里面的password
* 与用户请求登陆的密码做一个比对
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
配置
在BrowserSecurityConfig配置类中,添加一个Bean
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
//对密码进行加密
//如果要自定义加密方法,只需要写一个类实现PasswordEncoder,并重写encoder和match方法,再注册到Spring容器中。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
//这是Security提供的一个PasswordEncoder的实现类
}
//Security配置
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.and()
.authorizeRequests() //认证请求
.anyRequest() //任何请求
.authenticated(); //都需要身份认证
}
}
此时,我们再次访问/hello,会发现密码竟然不正确了,这是因为我们返回的UserDetails中的密码还是原来的明文,但我们又使用了PasswordEncoder来对密码进行加密,所以我们需要将返回的UserDetails中的密码进行加密后再返回。
所我们要对MyUserDetailsService进行一些修改
@Component
public class MyUserDetailsService implements UserDetailsService {
//根据自己使用的ORM,可以注入dao或mapper对象到数据库里查出用户信息
//这里为了简化demo演示,就先不涉及数据库的内容了。
@Autowired
private PasswordEncoder passwordEncoder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查找用户信息
logger.info("登陆用户名" + username);
String password = passwordEncoder.encode("123456"); //正常是用户注册的时候存入数据库中的,然后取出来。
logger.info("加密后的密码" + password);
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
再启动应用,访问/hello
这次登陆成功了,在这里,我重复登陆了两次,可以看到每次加密后的密码都是不一样的,这是为了防止密码被破解,Spring Security对密码加了盐值,防止同一个密码加密后的数字被破解,简单说就是为了使相同的密码拥有不同的hash值的一种手段。
Q.E.D.