关于java:Spring-Security认证流程分析练气后期

4次阅读

共计 11581 个字符,预计需要花费 29 分钟才能阅读完成。

写在后面

在前一篇文章中,咱们介绍了如何配置 spring security 的自定义认证页面,以及前后端拆散场景下如何获取 spring security 的 CSRF Token。在这一篇文章中咱们未来剖析一下 spring security 的认证流程。
提醒:我应用的 spring security 的版本是 5.3.4.RELEASE。如果读者应用的不是和我同一个版本,源码轻微之处有些不同,然而大体流程都是一样的。

认证流程剖析

通过查阅 spring security 的官网文档咱们晓得,spring security 的认证过滤操作由 UsernamePasswordAuthenticationFilter 实现。那么,咱们这次的流程剖析就从这个过滤器开始。

UsernamePasswordAuthenticationFilter

先上局部源码

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {super(new AntPathRequestMatcher("/login", "POST"));
    }


    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
// 1. 必须为 POST 申请
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod());
        }
//2. 取出用户填写的用户名和明码
        String username = obtainUsername(request);
        String password = obtainPassword(request);
//3. 防止出现空指针
        if (username == null) {username = "";}

        if (password == null) {password = "";}
        //4. 去掉用户名的空格
        username = username.trim();
        //5. 在层层校验后,开始对 username 和 password 进行封装
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        // 6. 认证逻辑
        return this.getAuthenticationManager()
            .authenticate(authRequest);
    }
}

从下面的剖析咱们晓得了,当表单信息进入到这个过滤器之后,通过层层校验,将其封装成 UsernamePasswordAuthenticationToken 对象。接下来咱们进入到这个对象外面看看。

一下是局部源码

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 530L;
   // 用户名
   private final Object principal;
   // 明码
   private Object credentials;

    //5.1 还未认证,走这个构造方法
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        
        this.setAuthenticated(false);
    }
}

AuthenticationManager

在上方第 6 步,进入了认证逻辑,(真正认证操作在 AuthenticationManager 外面)咱们接下来进入到 AuthenticationManager 对象的 authenticate()办法里看看。

发现这是一个接口。从图中能够晓得除了 ProviderManager 这个类之外,其余的都是外部类,所有咱们就间接进入到 ProviderManager 对象的 authenticate 办法里看看

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
//7. 找到与之对应的认证形式(本零碎账户登录。。微信登录等)for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}

            if (debug) {
                logger.debug("Authentication attempt using"
                        + provider.getClass().getName());
            }
//8。调用认证服务提供者的办法进行认证
            try {result = provider.authenticate(authentication);

                if (result != null) {copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            } catch (AuthenticationException e) {lastException = e;}
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {lastException = parentException = e;}
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();}

            // If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
            if (parentResult == null) {eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        // If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
        if (parentException == null) {prepareException(lastException, authentication);
        }

        throw lastException;
    }
// spring security 将其所有认证形式都封装成一个 AuthenticationProvider 汇合,第一步便是找出对应的认证形式
public List<AuthenticationProvider> getProviders() {return providers;}

}

AuthenticationProvider

在步骤 8 中,调用了认证提供者的认证办法,接下来咱们进去看看。发现 AuthenticationProvider 是一个接口

咱们从实现类的名称当中猜一个进去看看,就看 AbstractUserDetailsAuthenticationProvider 这个类。

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
//8.1 尝试从缓存中获取用户
        boolean cacheWasUsed = true;
    //UserDetails 就是 spring Security 内定义的用户对象
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;
//8.2 如果缓存中不存在用户,则开始检索
            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;}
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        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;}
        }

        postAuthenticationChecks.check(user);

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

        Object principalToReturn = user;

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

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

在步骤 8.2 中,调用了 retrieveUser 办法查找用户,接下来咱们进去看看

protected abstract UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;

发现它是一个形象的办法,接下来点进去,看看它曾经提供好的实现办法。这个办法在 DaoAuthenticationProvider 对象中

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {prepareTimingAttackProtection();
        try {
            //8.2.1 通过用户名加载用户
            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 对象的 loadUserByUsername(办法去做加载操作,咱们点进去看看

UserDetailsService

public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

发现这是一个接口,并且到了这一步就失去了咱们的用户对象 UserDetails。如果说大家要自定义认证信息检索,查找本人定义的 User 对象话就实现这个接口,并且让本人的用户对象实现 UserDetails 接口。并且实现相干查询方法和注册。

接下来咱们看 spring security 曾经提供好的实现类它的实现类

咱们重点关注的有两个,一个是 JdbcDaoImpl,一个是 CachingUserDetailsService。前者从数据库中查问用户,后者从缓存中查问用户信息

咱们先看 CachingUserDetailsService 的源码

public class CachingUserDetailsService implements UserDetailsService {private UserCache userCache = new NullUserCache();
    private final UserDetailsService delegate;

    public CachingUserDetailsService(UserDetailsService delegate) {this.delegate = delegate;}

    public UserCache getUserCache() {return userCache;}

    public void setUserCache(UserCache userCache) {this.userCache = userCache;}

    public UserDetails loadUserByUsername(String username) {UserDetails user = userCache.getUserFromCache(username);

        if (user == null) {user = delegate.loadUserByUsername(username);
        }

        Assert.notNull(user, () -> "UserDetailsService" + delegate
                + "returned null for username" + username + "."
                + "This is an interface contract violation");

        userCache.putUserInCache(user);

        return user;
    }
}

再看 JdbcDaoImpl(局部)

public class JdbcDaoImpl extends JdbcDaoSupport
        implements UserDetailsService, MessageSourceAware {
@Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {List<UserDetails> users = loadUsersByUsername(username);

        if (users.size() == 0) {this.logger.debug("Query returned no results for user'" + username + "'");

            throw new UsernameNotFoundException(
                    this.messages.getMessage("JdbcDaoImpl.notFound",
                            new Object[] { username}, "Username {0} not found"));
        }

        UserDetails user = users.get(0); // contains no GrantedAuthority[]

        Set<GrantedAuthority> dbAuthsSet = new HashSet<>();

        if (this.enableAuthorities) {dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
        }

        if (this.enableGroups) {dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
        }

        List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);

        addCustomAuthorities(user.getUsername(), dbAuths);

        if (dbAuths.size() == 0) {
            this.logger.debug("User'" + username
                    + "'has no authorities and will be treated as'not found'");

            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[] {username},
                    "User {0} has no GrantedAuthority"));
        }

        return createUserDetails(username, user, dbAuths);
    }
    
protected List<UserDetails> loadUsersByUsername(String username) {return getJdbcTemplate().query(this.usersByUsernameQuery,
                new String[] { username}, (rs, rowNum) -> {String username1 = rs.getString(1);
                    String password = rs.getString(2);
                    boolean enabled = rs.getBoolean(3);
                    return new User(username1, password, enabled, true, true, true,
                            AuthorityUtils.NO_AUTHORITIES);
                });
    }
        

这两个获取形式的逻辑都比较简单,置信大家能看的明确。

略微总结一下:

  1. UsernamePasswordAuthenticationFilter拦挡到用户填写的表单信息后,先进行校参解决(判断申请是否为 POST 申请,将 null 值转为空字符串),而后将参数封装成 UsernamePasswordAuthenticationToken(这是一个 Authentication 实现类 AbstractAuthenticationToken 的子类)对象,再而后调用 AuthenticationManager 对象的实现类 ProviderManager 的 authenticate 办法进行认证操作;
  2. ProviderManager 在接管到 token 后,先依据 token 的 className 比对 spring security 内置的认证形式,找到后调用 AuthenticationProvider 的实现类 AbstractUserDetailsAuthenticationProvider 的 authenticate 办法进行认证操作
  3. AbstractUserDetailsAuthenticationProvider 对象在收到 Authentication 对象后,先确定用户名,再依据用户名从缓存里查找用户信息,找不到则调用 retrieveUser 办法在长久层查找数据(长久层数据能够是文本、数据库里的数据)。在 spring security 中,只有 DaoAuthenticationProvider 实现了这个办法(目前为止)。这时 DaoAuthenticationProvider 便调用 UserDetailsService 的 loadUserByUsername 办法找到 userDetails。在通过了一系列的判断验证后,调用 createSuccessAuthentication 办法给受权,并将其(UsernamePasswordAuthenticationToken)返回给了 AuthenticationManager 的实现类 ProviderManager。
  4. ProviderManager 在收到 UsernamePasswordAuthenticationToken 对象后,先进行参数校验(判空,判 null),之后调用事件发布者 eventPublisher 的 publishAuthenticationSuccess 办法将验证后果公布进来。最初将后果返回给 UsernamePasswordAuthenticationFilter。至此验证流程大体上就完结了.

也就述说,UsernamePasswordAuthenticationFilter 负责拦挡,AuthenticationManager 负责组织流程,真正执行操作的是认证 AuthenticationProvider 的子类 AbstractUserDetailsAuthenticationProvider 对象。

End

给大家画了一张简化版的认证时序图

正文完
 0