这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
写在前面
通过前几篇的原理剖析,我们这一篇来一篇很干的理论实战,那就是如何自定义用户认证,
实现自定义用户认证的过程通常涉及两大部分内容,
一方面需要使用 User 和 Authority 对象来完成定制化的用户管理
另一方面需要把这个定制化的用户管理嵌入整个用户认证流程中。下面我们分别详细分析。
实现用户管理
我们知道在 Spring Security 中,代表用户信息的就是 UserDetails 接口。我们在前几篇文章中也介绍过 UserDetails 接口的具体定义。如果你想实现自定义的用户信息,扩展这个接口即可。实现方式如下所示:
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities().stream()
.map(a -> new SimpleGrantedAuthority(a.getName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public final User getUser() {
return user;
}
}
上述 CustomUserDetails 类实现了 UserDetails 接口中约定的所有需要实现的方法。请注意,这里的 getAuthorities() 方法中,我们将 User 对象中的 Authority 列表转换为了 Spring Security 中代表用户权限的SimpleGrantedAuthority 列表。
当然,所有的自定义用户信息和权限信息都是维护在数据库中的,所以为了获取这些信息,我们需要创建数据访问层组件,这个组件就是 UserRepository,定义如下:
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findUserByUsername(String username);
}
这里只是简单扩展了 Spring Data JPA 中的 JpaRepository 接口,并使用方法名衍生查询机制定义了根据用户名获取用户信息的 findUserByUsername 方法。
现在,我们已经能够在数据库中维护自定义用户信息,也能够根据这些用户信息获取到 UserDetails 对象,那么接下来要做的事情就是扩展 UserDetailsService。自定义 CustomUserDetailsService 实现如下所示:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public CustomUserDetails loadUserByUsername(String username) {
Supplier<UsernameNotFoundException> s =
() -> new UsernameNotFoundException("Username" + username + "is invalid!");
User u = userRepository.findUserByUsername(username).orElseThrow(s);
return new CustomUserDetails(u);
}
}
这里我们通过 UserRepository 查询数据库来获取 CustomUserDetails 信息,如果传入的用户名没有对应的 CustomUserDetails 则会抛出异常。
实现认证流程
我们再次回顾 AuthenticationProvider 的接口定义,如下所示:
public interface AuthenticationProvider {
//执行认证,返回认证结果
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//判断是否支持当前的认证对象
boolean supports(Class<?> authentication);
}
实现自定义认证流程要做的也是实现 AuthenticationProvider 中的这两个方法,而认证过程势必要借助于前面介绍的 CustomUserDetailsService。
我们先来看一下 AuthenticationProvider 接口的实现类 AuthenticationProviderService,如下所示:
@Service
public class AuthenticationProviderService implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private SCryptPasswordEncoder sCryptPasswordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
//根据用户名从数据库中获取 CustomUserDetails
CustomUserDetails user = userDetailsService.loadUserByUsername(username);
//根据所配置的密码加密算法分别验证用户密码
switch (user.getUser().getPasswordEncoderType()) {
case BCRYPT:
return checkPassword(user, password, bCryptPasswordEncoder);
case SCRYPT:
return checkPassword(user, password, sCryptPasswordEncoder);
}
throw new BadCredentialsException("Bad credentials");
}
@Override
public boolean supports(Class<?> aClass) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
if (encoder.matches(rawPassword, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
} else {
throw new BadCredentialsException("Bad credentials");
}
}
}
1、AuthenticationProviderService 类虽然看起来比较长,但代码基本都是自解释的。我们首先通过 CustomUserDetailsService 从数据库中获取用户信息并构造成 CustomUserDetails 对象。然后,根据指定的密码加密器对用户密码进行验证。
2、如果验证通过则构建一个UsernamePasswordAuthenticationToken 对象并返回,反之直接抛出 BadCredentialsException 异常。而在 supports() 方法中指定的就是这个目标 UsernamePasswordAuthenticationToken 对象。
好了。今天就学到这里,我们下期学一下安全配置,一点一点学,每次看太多可能也消化不了,最后多说一句。欢迎大家点击我头像查看Security专栏(设计模式专栏已完结),目前正在进行的并发队列专题和Security的专题
弦外之音
感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流
加油! 我们下期再见!
给大家分享几个我前面写的几篇骚操作