Spring Security专栏(自定义用户认证)

765 阅读4分钟

这是我参与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的专题

弦外之音

感谢你的阅读,如果你感觉学到了东西,麻烦您点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

聊聊不一样的策略模式(值得收藏)

copy对象,这个操作有点骚!