捋一遍登录Spring Security登录流程(源码篇) - 图1
    在SpringSecurity一系列的过滤器链中,与认证相关的过滤器就是UsernamePasswordAuthenticationFilter,它继承自AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilterdoFilter方法在其父类AbstractAuthenticationProcessingFilter中被声明。如果我们想要在SpringSecurity自定义一个登录验证码或者将登录参数改成 JSON 的时候,我们都需要自定义过滤器继承自AbstractAuthenticationProcessingFilter,毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication方法就是在AbstractAuthenticationProcessingFilter类的doFilter方法中被触发的。

    1. public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean {
    2. private AuthenticationManager authenticationManager;
    3. private RequestMatcher requiresAuthenticationRequestMatcher;
    4. private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    5. private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
    6. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    7. throws IOException, ServletException {
    8. HttpServletRequest request = (HttpServletRequest) req;
    9. HttpServletResponse response = (HttpServletResponse) res;
    10. if (!requiresAuthentication(request, response)) {
    11. chain.doFilter(request, response);
    12. return;
    13. }
    14. if (logger.isDebugEnabled()) {
    15. logger.debug("Request is to process authentication");
    16. }
    17. Authentication authResult;
    18. try {
    19. authResult = attemptAuthentication(request, response);
    20. if (authResult == null) {
    21. // return immediately as subclass has indicated that it hasn't completed
    22. // authentication
    23. return;
    24. }
    25. sessionStrategy.onAuthentication(authResult, request, response);
    26. }
    27. catch (InternalAuthenticationServiceException failed) {
    28. logger.error(
    29. "An internal error occurred while trying to authenticate the user.",
    30. failed);
    31. unsuccessfulAuthentication(request, response, failed);
    32. return;
    33. }
    34. catch (AuthenticationException failed) {
    35. // Authentication failed
    36. unsuccessfulAuthentication(request, response, failed);
    37. return;
    38. }
    39. // Authentication success
    40. if (continueChainBeforeSuccessfulAuthentication) {
    41. chain.doFilter(request, response);
    42. }
    43. successfulAuthentication(request, response, chain, authResult);
    44. }
    45. }
    1. 首先判断当前请求是否需要认证?

      1. protected boolean requiresAuthentication(HttpServletRequest request,
      2. HttpServletResponse response) {
      3. return requiresAuthenticationRequestMatcher.matches(request);
      4. }
    2. 如果需要认证,则执行子类中重写的attemptAuthentication方法:

      1. public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
      2. public Authentication attemptAuthentication(HttpServletRequest request,
      3. HttpServletResponse response) throws AuthenticationException {
      4. if (postOnly && !request.getMethod().equals("POST")) {
      5. throw new AuthenticationServiceException(
      6. "Authentication method not supported: " + request.getMethod());
      7. }
      8. String username = obtainUsername(request);
      9. String password = obtainPassword(request);
      10. if (username == null) {
      11. username = "";
      12. }
      13. if (password == null) {
      14. password = "";
      15. }
      16. username = username.trim();
      17. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
      18. username, password);
      19. // Allow subclasses to set the "details" property
      20. setDetails(request, authRequest);
      21. return this.getAuthenticationManager().authenticate(authRequest);
      22. }
      23. }

      从这段源码可以看出:

    • 如果当前请求不是post请求,则会抛出异常。
    • 首先通过obtainUsernameobtainPassword方法提取请求里面的用户名和密码,提取方式为 request.getParameter,这也就是为什么默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数,如果想传递 JSON 参数,需要修改这里的逻辑。
    • 使用获取到的用户名和密码构建成一个UsernamePasswordAuthenticationToken对象,其中 username对应它的principal属性,password对应它的credentials属性。
    • 接下来 setDetails 方法给 details 属性赋值,UsernamePasswordAuthenticationToken本身没有 details 属性,这个属性在它的父类AbstractAuthenticationToken中。details 是一个对象,这个对象里面放的是webAuthenticationDetails实例,该实例主要描述了两个信息,请求的 remoteAddress 以及请求的 sessionId。

      1. public WebAuthenticationDetails(HttpServletRequest request) {
      2. this.remoteAddress = request.getRemoteAddr();
      3. HttpSession session = request.getSession(false);
      4. this.sessionId = (session != null) ? session.getId() : null;
      5. }
    • 最后一步开始做校验,校验操作首先要获取到一个AuthenticationManager,这是一个接口,这里获取到的是它的实现类ProviderManager,然后调用authenticate方法:

    捋一遍登录Spring Security登录流程(源码篇) - 图2

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Class<? extends Authentication> toTest = authentication.getClass();
    4. for (AuthenticationProvider provider : getProviders()) {
    5. if (!provider.supports(toTest)) {
    6. continue;
    7. }
    8. result = provider.authenticate(authentication);
    9. if (result != null) {
    10. copyDetails(authentication, result);
    11. break;
    12. }
    13. }
    14. if (result == null && parent != null) {
    15. result = parentResult = parent.authenticate(authentication);
    16. }
    17. if (result != null) {
    18. if (eraseCredentialsAfterAuthentication
    19. && (result instanceof CredentialsContainer)) {
    20. ((CredentialsContainer) result).eraseCredentials();
    21. }
    22. if (parentResult == null) {
    23. eventPublisher.publishAuthenticationSuccess(result);
    24. }
    25. return result;
    26. }
    27. throw lastException;
    28. }
    • 首先获取authentication的 class,判断当前provider是否支持该authentication
    • 如果支持的话,则调用providerauthenticate方法开始做校验,校验完成之后,会返回一个新的Authentication,一会儿再详细捋一下这个方法的具体逻辑。
    • 如果其中某一个provider认证成功,则调用copyDetails方法吧旧的token中的details属性拷贝到新的token中来;如果所有的provider都认证失败,则调用parentauthenticate方法继续进行校验。
    • 接下来调用eraseCredentials方法擦除凭证信息(也就是密码),这个擦除方法比较简单,就是将token中的credentials属性置空。

    大致的流程就是上面这样,在for循环中,第一次拿到的provider是一个AnonymousAuthenticationProvider,这个provider只支持AnonymousAuthenticationToken,压根不支持UsernamePasswordAuthenticationToken,也就是会直接在provider中的supports方法中返回 false,结束for循环,然后会进入到下一个 if 中,直接调用parentauthenticate方法进行校验。而parent就是providerManager,所以会再次回到这个authenticate方法中。此时provider也变成了DaoAuthenticationProvider,这个provider是支持UsernamePasswordAuthenticationToken,所以会顺利进入该类的authenticate方法去执行,而DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider并且没有重写authenticate方法,所以,我们最终来到AbstractUserDetailsAuthenticationProvider#authenticate方法中:

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    4. : authentication.getName();
    5. try {
    6. user = retrieveUser(username,
    7. (UsernamePasswordAuthenticationToken) authentication);
    8. }
    9. catch (UsernameNotFoundException notFound) {
    10. logger.debug("User '" + username + "' not found");
    11. if (hideUserNotFoundExceptions) {
    12. throw new BadCredentialsException(messages.getMessage(
    13. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    14. "Bad credentials"));
    15. }
    16. else {
    17. throw notFound;
    18. }
    19. }
    20. preAuthenticationChecks.check(user);
    21. additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
    22. postAuthenticationChecks.check(user);
    23. Object principalToReturn = user;
    24. if (forcePrincipalAsString) {
    25. principalToReturn = user.getUsername();
    26. }
    27. return createSuccessAuthentication(principalToReturn, authentication, user);
    28. }
    • 首先从Authentication对象中获取处登录用户名。
    • 然后拿着username去调用子类DaoAuthenticationProbviderretrieveUser方法获取当前用户对象,这一步会使用UserDetailsService接口中的loadUserByUsername方法返回user。可以通过实现UserDetailsService接口重写loadUserByUsername方法来从数据库中读取用户,SpringSecurity默认使用的是InMemoryUserDetailsManager实现类中的loadUserByUsername方法,从内存中获取用户。
    • 在查找用户的时候,如果抛出了UsernameNotFoundException,这个异常被捕获,捕获之后,如果hideUserNotFoundExceptions属性的值为 true,就抛出一个BadCredentialsException。相当于将UsernameNotFoundException异常隐藏了,而默认情况下,hideUserNotFoundExceptions的值就为 true。所以无论用户是账号还是密码错误,收到的都是BadCredentialsException异常。
    • 接下来调用preAuthenticationChecks#check方法来检验user中的各个账户状态属性是否正常,如账户是否被锁定、账户是否被禁用、账户是否过期等等。
    • additionalAuthenticationChecks方法则是做密码比对的,使用PasswordEncoder中的matches方法比对token中的credentials属性和读取出来的用户中的密码是否相同,如果不同,则会抛出BadCredentialsException异常。
    • 最后在postAuthenticationChecks.check方法中检查密码是否过期。
    • 接下来有一个forcePrincipalAsString属性,这个是是否强制将Authentication中的principal属性设置为字符串,这个属性一开始在UsernamePasswordAuthenticationFilter类中其实就是设置为字符串(即 username),但是默认情况下,当用户登录成功后,这个属性就变成当前用户这个对象了。之所以会这样,就是因为forcePrincipalAsString默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。
    • 最后,通过createSuccessAuthentication方法构建一个新的UsernamePasswordAuthenticationToken
    1. AbstractAuthenticationProcessingFilter#doFilter方法的源码中可以知道,当登录成功的时候,successfulAuthentication方法则会被调用:

      1. protected void successfulAuthentication(HttpServletRequest request,
      2. HttpServletResponse response, FilterChain chain, Authentication authResult)
      3. throws IOException, ServletException {
      4. SecurityContextHolder.getContext().setAuthentication(authResult);
      5. rememberMeServices.loginSuccess(request, response, authResult);
      6. // Fire event
      7. if (this.eventPublisher != null) {
      8. eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
      9. authResult, this.getClass()));
      10. }
      11. successHandler.onAuthenticationSuccess(request, response, authResult);
      12. }

      在这里有一段很重要的代码,就是SecurityContextHolder.getContext().setAuthentication(authResult);,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从SecurityContextHolder.getContext()中获取到,想修改,也可以在这里修改。最后,大家还看到一个successHandler.onAuthenticationSuccess,这就是在SecurityConfig中配置的登录成功回调方法。

    2. 认证失败

      1. protected void unsuccessfulAuthentication(HttpServletRequest request,
      2. HttpServletResponse response, AuthenticationException failed)
      3. throws IOException, ServletException {
      4. SecurityContextHolder.clearContext();
      5. if (logger.isDebugEnabled()) {
      6. logger.debug("Authentication request failed: " + failed.toString(), failed);
      7. logger.debug("Updated SecurityContextHolder to contain null Authentication");
      8. logger.debug("Delegating to authentication failure handler " + failureHandler);
      9. }
      10. rememberMeServices.loginFail(request, response);
      11. failureHandler.onAuthenticationFailure(request, response, failed);
      12. }

      SecurityContextHolder.clearContext();清空认证信息;以及failureHandler.onAuthenticationFailure,这就是在SecurityConfig中配置的登录失败回调方法。