相关文章推荐

之前看过很多个版本的 Spring Security 中获取用户信息并进行密码校验,有在相关的Filter中获取的,也有在默认的 DaoAuthenticationProvider 中判断 Authentication 类型进行判断以后直接获取的。

不过,这些方式都不太“正宗”, Spring Security 有自己的一套流程。至于此流程的详细信息讲解,先不急,本篇文章来认识一下其中比较重要的一环:获取用户信息并进行密码验证,即 DaoAuthenticationProvider

AuthenticationProvider

先来认识一下其实现的接口 AuthenticationProvider ,主要用于解析特定 Authentication

* Indicates a class can process a specific * {@link org.springframework.security.core.Authentication} implementation.

其中,有两个接口方法。

首先是 authenticate(Authentication authentication) 方法。

Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

该方法与 AuthenticationManager 中的 authenticate 声明及功能完全一致,返回包含凭据的完整身份验证对象 authentication 。但是,如果 AuthenticationProvider 不支持给定的 Authentication 的话,该方法可能会返回 null 。在此情况下,下一个支持 authentication AuthenticationProvider 将会被尝试(关于此段逻辑, AuthenticationManager 中会有详细的逻辑代码及说明,后续再详细讲解)。

其次是 supports(Class<?> authentication) 方法。

boolean supports(Class<?> authentication);

如果 AuthenticationProvider 支持给定的 Authentication 的话,会返回 true 。但是,这并不保证 AuthenticationProvider 能够对给定的 Authentication 进行身份认证,它只是表明它可以支持对其进行更深入的评估, AuthenticationProvider 依然可以返回 null ,以指示应尝试另一个 AuthenticationProvider

此方法是用以选择一个能够匹配 Authentication 以胜任身份认证工作的 AuthenticationProvider ,交给 ProviderManager 来执行。

AbstractUserDetailsAuthenticationProvider

用于解析 UsernamePasswordAuthenticationToken 以进行身份认证的基础 AuthenticationProvider 。其子类可以重写或使用其 UserDetails 对象。

其实现了 AuthenticationProvider 接口的 authenticate(Authentication authentication) 方法,其认证过程可分为一下几个步骤。

获取用户名。

String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

缓存中获取 User。

boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

如果缓存中获取的 User 为 null,再调用 retrieveUser (子类实现)方法获取。

if (user == null) {
    cacheWasUsed = false;
    try {
        user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
    catch (UsernameNotFoundException notFound) {
        logger.debug("User '" + username + "' not found");
        if (hideUserNotFoundExceptions) {
            throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
        else {
            throw notFound;
    ......

如果找不到用户,会抛出 UsernameNotFoundException 异常。此时,处理方案有2个。

  • 如果 hideUserNotFoundExceptions 为 true默认为 true),即隐藏用户未找到异常,则会重新抛出凭据/密码错误异常,异常信息为 Spring Security 框架已定义好的提示信息。

  • 如果不隐藏用户未找到异常,则直接抛出 UsernameNotFoundException 异常。

前置身份认证检查。

preAuthenticationChecks.check(user);

默认的前置身份认证检查逻辑如下,即事先校验一下用户的账号是否锁定、是否可用、是否过期

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        if (!user.isAccountNonLocked()) {
            logger.debug("User account is locked");
            throw new LockedException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.locked",
                "User account is locked"));
        if (!user.isEnabled()) {
            logger.debug("User account is disabled");
            throw new DisabledException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.disabled",
                "User is disabled"));
        if (!user.isAccountNonExpired()) {
            logger.debug("User account is expired");
            throw new AccountExpiredException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.expired",
                "User account has expired"));

额外身份认证校验,对于 UsernamePasswordAuthenticationToken 来说就是凭据/密码校验。

try {
    preAuthenticationChecks.check(user);
    additionalAuthenticationChecks(user,
           (UsernamePasswordAuthenticationToken) authentication);
catch (AuthenticationException exception) {
    if (cacheWasUsed) {
        // There was a problem, so try again after checking
        // we're using latest data (i.e. not from the cache)
        cacheWasUsed = false;
        user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                                       (UsernamePasswordAuthenticationToken) authentication);
    else {
        throw exception;

如果认证过程中发生异常,会有如下处理逻辑:

  1. 如果当前的用户是从用户缓存中取出的,则使用原有的用户信息再进行一次身份认证,即获取用户信息、前置身份认证检查、额外身份认证检查;

  2. 如果不是从用户缓存中取出的,则直接抛出异常。

具体的校验逻辑在下面的 DaoAuthenticationProvider 中会进行详细说明,此处暂且不谈。

后置身份认证检查。

postAuthenticationChecks.check(user);

默认的后置身份认证检查逻辑如下,即检查一下用户的凭据/密码是否过期。

private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        if (!user.isCredentialsNonExpired()) {
            logger.debug("User account credentials have expired");
            throw new CredentialsExpiredException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                "User credentials have expired"));

将当前用户放入用户缓存,如果当前用户还没有被用户缓存缓存的话。

if (!cacheWasUsed) {
    this.userCache.putUserInCache(user);

转换 principal 为字符串类型。不过,需要 forcePrincipalAsString 参数为 true(默认为 false)。

Object principalToReturn = user;
if (forcePrincipalAsString) {
    principalToReturn = user.getUsername();

最后,创建身份认证成功的 Authentication

return createSuccessAuthentication(principalToReturn, authentication, user);

默认的创建身份认证成功的 Authentication 逻辑如下。

return createSuccessAuthentication(principalToReturn, authentication, user);

即创建了一个新的 UsernamePasswordAuthenticationToken,与未认证的区别就是 principal 变成了检索到的用户详细信息(或者用户名,强制字符串principal

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

AbstractUserDetailsAuthenticationProvider 的主要功能就是这些,剩下的,就是诸如 retrieveUser 、additionalAuthenticationChecks 等需要子类实现的抽象方法了,这就是属于子类 DaoAuthenticationProvider 的部分了。

DaoAuthenticationProvider

说到 Authentication 相信都不陌生。最著名的,便是 UsernamePasswordAuthenticationToken。而 DaoAuthenticationProvider 便是用于解析并认证 UsernamePasswordAuthenticationToken 的这样一个认证服务提供者。

类注释说的也很明白。

* An {@link AuthenticationProvider} implementation that retrieves user details from a * {@link UserDetailsService}.

其最终目的,就是根据 UsernamePasswordAuthenticationToken,获取到 username,然后调用 UserDetailsService 检索用户详细信息。

在其基类 AbstractUserDetailsAuthenticationProvider 中,我们已经讲过,需要子类实现 retrieveUser 、additionalAuthenticationChecks 等抽象方法那么,我们就来详细看一下吧

额外的身份认证检查,也即 additionalAuthenticationChecks,密码检查。

protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
    String presentedPassword = authentication.getCredentials().toString();
    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");
        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));

这里的检查逻辑比较简单,就不再赘述了。

接下来就是用户检索,即 retrieveUser

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        return loadedUser;
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);

正如其类注释所述,需要调用 UserDetailsService 检索用户详细信息,如权限列表、存储密码等,逻辑也比较简单。但是,有两个特殊方法需要注意一下,即检索用户前调用的 prepareTimingAttackProtection() 方法,和抛出 UsernameNotFoundException 异常后调用的 mitigateAgainstTimingAttack(authentication) 方法。

这两个方法都是用于定时攻击保护的。

首先是 prepareTimingAttackProtection() 方法。

private void prepareTimingAttackProtection() {
    if (this.userNotFoundEncodedPassword == null) {
        this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);

该方法从方法命名上理解,是为了准备定时攻击保护用的。其实,就是将 userNotFoundEncodedPassword,也即用户未找到时的加密密码给准备好,然后使用配置的 passwordEncoder 来加密为密文,默认的用户未找到时的明文密码为 userNotFoundPassword。

private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

其次是 mitigateAgainstTimingAttack(authentication) 方法。

private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
    if (authentication.getCredentials() != null) {
        String presentedPassword = authentication.getCredentials().toString();
        this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);

逻辑也不复杂,就是在用户提供的凭据/密码不为空时,使用配置的 passwordEncoder 来验证两者是否相等。看到这里也许会有疑惑,这两者相等的概率太小了吧?一个是用户随便填的,一个是系统配置的,且无论相同与否,都不影响下面 UsernameNotFoundException 异常的抛出,除非我们在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常。

这里我们看一下方法命名中的 mitigate,意味缓解、减轻、缓和。

什么意思呢?我们需要从整体上想象一下。

用户登录时,调用 AuthenticationProvider 的 authenticate 方法,然后,先从用户缓存中获取用户,如果取不到,再调用 retrieveUser 方法检索用户。最后,再调用 additionalAuthenticationChecks 方法进行密码检查。既然定性为有目的的定时攻击,那么在发生此种情况后,就没有必要继续验证密码了,进一步也没有必要进行后续的身份认证处理了。

至于是否在自定义的 passwordEncoder 中抛出另外一个非 UsernameNotFoundException 异常,还是存疑的,感觉实在没有必要,这里倾向于直接返回密码检查结果,至于 UsernameNotFoundException 异常,在基类调用retrieveUser 方法是已有相关异常捕获逻辑,且可以自行配置是否隐藏该异常类型,所以,感觉没有必要。

这里也确实猜不透设计团队的意图,存个疑吧。有了解此番意图的,可联系作者说明,不胜感激!

另外,DaoAuthenticationProvider 还重写了基类的 createSuccessAuthentication 方法。

@Override
protected Authentication createSuccessAuthentication(Object principal,
                                                     Authentication authentication, UserDetails user) {
    boolean upgradeEncoding = this.userDetailsPasswordService != null
        && this.passwordEncoder.upgradeEncoding(user.getPassword());
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    return super.createSuccessAuthentication(principal, authentication, user);

还记得我们在介绍基类的 createSuccessAuthentication 方法时所说的逻辑吗?

当然,此方法是 protected 类型的,子类可以重写。因为,子类通常在 Authentication 中存储用户提供的原始凭据/密码,而非加盐、加密过的。

该方法便是对此段逻辑描述最好的呈现了。

至于 userDetailsPasswordService,其实也很简单,就是更新一下 User 中的密码,仅此而已。

public UserDetails updatePassword(UserDetails user, String newPassword) {
    String username = user.getUsername();
    MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
    mutableUser.setPassword(newPassword);
    return mutableUser;

自定义Provider

还记得前一篇文章中自定义的适用于CA登录认证的 CertificateAuthorityAuthenticationToken 和 CertificateAuthorityAuthenticationFilter 吗?

相对应的,这里我们就自定义个适用于CA登录认证的 CertificateAuthorityAuthenticationProvider。

public class CertificateAuthorityDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private UserDetailsService userDetailsService;
    public CertificateAuthorityDaoAuthenticationProvider() {
    @Override
    public boolean supports(Class<?> authentication) {
        return (CertificateAuthorityAuthenticationToken.class.isAssignableFrom(authentication));
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        // do nothing.
  ......

由于CA登录时,其CA KEY的密码是有前端配合控制器校验的,况且其密码也不在业务数据库中存储,不需要Spring Security框架处理,所以,无需进行额外的身份认证检查。

另外,需要修改一下 supports 方法,仅支持 CertificateAuthorityAuthenticationToken 类型的 Authentication 哦。

其它详细源码,请参考文末源码链接,可自行下载后阅读。

github

https://github.com/liuminglei/SpringSecurityLearning/tree/master/26

gitee

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/26
 

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring
文章目录DaoAuthenticationProviderDeclaredJdocretrieveUserDeclaredMethod CodeadditionalAuthenticationChecksDeclaredMethod CodecreateSuccessAuthenticationDeclaredMethod Code DaoAuthenticationProvider Declar...
protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(new LoginAuthenticationProvider(loginService));
`Spring Security`作为一个功能完善的安全框架,具有以下特性: - **认证(Authentication)**:解决 "你是谁" 的问题,验证系统中是否有这个“用户”(用户/设备/系统),也就是我们常说的“登录”。 - **授权(Authorization)**:权限控制/鉴别,解决的是系统中某个用户能够访问哪些资源,即“你能干什么”的问题。`Spring Security` 支持基于 `URL` 的请求授权、方法访问授权、对象访问授权。
正文有三种情况我们需要配置多个AuthenticationProvider,甚至是自定义AuthenticationProvider: 1. 用户认证信息存储在多个地方 2. 在同一个地方但是需要多种授权方式,比如说手机号+密码可以登录,邮箱+密码也可以登录 3. 以上两种情况都有 配置AuthenticationProvider,可以通过AuthenticationManagerBuild
AbstractUserDetailsAuthenticationProvider类位于org.springframework.security.authentication.dao包下,在 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter中被调用, 该类的public Au...
开发遇到一件很尴尬的事情,springboot里面使用security做登录验证,但是默认的就是验证username和password,现在的第三方或者短信登录非常流行。我现在的需求是做账号+短信登录(ps:后面还不知道要加啥… so:旧的不能废,只能扩展),接rongcloud sdk post:/login params:{ username:mobile, sessionid:rongclo...
 
推荐文章