写在前面 在前面学习前后端分离模式下的回调和注销时,提到了密码擦除,当时没有对SpringSecurity的登录流程进行梳理,那么本篇就来详细学习登录流程。
场景描述 现在有一个场景,用户在服务端安全管理选择了SpringSecurity,那么用户登录成功后,SpringSecurity会将用户信息保存在Session中,但是具体保存的位置,就目前而言开发者是不知道的,但是现在就是想知道这个信息的保存位置,以便当用户在前端修改了自己的信息,在不重新登录的情况下,开发者如何获取到用户的最新信息?这个场景在实际工作中是很常见的。
Authentication对象 如果你之前使用过Shiro框架,那么就知道在Shiro框架中与用户认证相关的信息都在AuthenticationToken接口中,查看一下这个接口的源码:
1 2 3 4 5 public interface AuthenticationToken extends Serializable { Object getPrincipal(); Object getCredentials(); }
它只有两个方法,一个获取用户名,一个获取用户密码。而我们常用于认证的UsernamePasswordToken类就是实现了这个接口,这个UsernamePasswordToken类中包含username、password、rememberMe和host这四个属性以及对应的getter和setetr方法。
其实SpringSecurity框架也存在类似的代码,所不同的是SpringSecurity框架中与认证相关的是Authentication接口,查看一下这个接口的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }
乍一看是不是有一种似曾相似的感觉?是的,前一篇《Spring Data JPA操作数据库》一文中在自定义UserDetails时,实现了UserDetails接口,查看一下这个UserDetails接口的源码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
是不是非常相似,第一个都是获取角色信息,之后是获取用户名和密码等信息。
回到Authentication接口中来,前面说过这个接口只有六个方法,分别用于获取角色信息、用户名、用户信息、密码、是否验证、设置验证等,因此开发者可以在需要使用的地方注入Authentication对象,进而获取当前登录用户的信息。顺便查看一下这个接口的实现类:
可以看到它存在6个实现类,如下所示:AbstractAuthenticationToken
、AnonymousAuthenticationToken
、JaasAuthenticationToken
、PreAuthenticatedAuthenticationToken
、RememberMeAuthenticationToken
、TestingAuthenticationToken
和UsernamePasswordAuthenticationToken
等,同样我们常用于认证的就是UsernamePasswordAuthenticationToken
类,查看一下这个UsernamePasswordAuthenticationToken
类的源码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 540L; private final Object principal; private Object credentials; public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } public Object getCredentials() { return this.credentials; } public Object getPrincipal() { return this.principal; } public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
可以看到这个类中有两个有参的构造方法、两个属性及其对应的getter/setter方法,这么看似鸡肋的类,其实主要功能还得看它继承的AbstractAuthenticationToken类。这两个属性principal和credentials其实就是用户名和密码,因此开发者是完全可以从这个类中获取用户名和密码。但是到现在为止,我们还是不知道登录信息是如何保存到这两个对象中的,仅仅知道可以从这个类中获取登录信息,为此我们需要对登录流程进行全面梳理。
登录流程 在Shiro框架中认证和授权的校验都是在一系列的过滤器链中完成的,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ System.out.println("******shiroFilter******"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //设置拦截器 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); //配置不会被拦截的链接,此时会按照顺序进行判断 //anon是匿名访问,authc是通过认证后才能访问 filterChainDefinitionMap.put("/static/**","anon"); //配置退出过滤器,具体已经由Shiro实现了 filterChainDefinitionMap.put("/logout","logout"); //配置过滤链,它会从上到下顺序执行,因此一般将/**放在最下面 filterChainDefinitionMap.put("/**","authc"); //如果不设置,则默认会自动寻找Web工程根目录下的login.html页面 shiroFilterFactoryBean.setLoginUrl("/login"); //设置登录成功后需要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //设置未授权页面 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
与此类似的,SpringSecurity的认证与授权也是在一系列的过滤器链中完成的,其中与认证相关的过滤器为UsernamePasswordAuthenticationFilter
,查看一下这个类的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); username = username != null ? username : ""; username = username.trim(); String password = this.obtainPassword(request); password = password != null ? password : ""; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
其实这个类在前面学习“自定义登录参数名称”的时候就已经简单学习过了,接下来对登录流程进行分析:(1) 调用attemptAuthentication
方法来进行尝试验证;(2) 之后(1)中通过obtainUsername
和obtainPassword
方法获取请求中的用户名和密码,其底层使用了request.getParameter()
方法。也就是说这两个方法都是从表单控件中获取登录信息,即需要通过key/value键值对来传递参数。请注意不能使用JOSN来传递参数,开发者如果想通过JOSN来传递参数,那么就需要修改此处逻辑。(3) 接着(1)传入username和password参数来构造一个UsernamePasswordAuthenticationToken
对象,查看一下这个对象的构造方法:
1 2 3 4 5 6 public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); }
也就是说这里的username其实就对应于UsernamePasswordAuthenticationToken
对象中的principal属性,password对应于UsernamePasswordAuthenticationToken
对象中的credentials属性。(4) 接下来(1)中调用setDetails方法,将request和UsernamePasswordAuthenticationToken
对象作为参数传入进去,但是细心的你可能发现这个UsernamePasswordAuthenticationToken
对象是没有details这个属性的,它没有这个属性,那就查看一下它的父类AbstractAuthenticationToken
,可以发现它的父类是有的:
1 2 3 4 5 6 7 8 9 private Object details; public Object getDetails() { return this.details; } public void setDetails(Object details) { this.details = details; }
现在问题来了,这个details对象中保存的是什么呢?查看一下UsernamePasswordAuthenticationFilter
类中setDetails方法的源码,如下所示:
1 2 3 protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); }
可以发现此处调用了authenticationDetailsSource
的buildDetails()
方法,查看一下源码:
1 2 3 public interface AuthenticationDetailsSource<C, T> { T buildDetails(C var1); }
也就是说最后得到的其实是一个WebAuthenticationDetails
对象,这个对象主要描述了请求的remoteAddress和sessionId信息。关于这一部分内容,会在后面《获取登录额外信息》一文中进行详细介绍。(5) 最后(1)中调用authenticate()
方法去做认证。
可以看到这里所说的登录流程,其实就是attemptAuthentication
方法的执行过程。
认证过程 说完了前面的登录流程,再来看后面的认证过程。我们从attemptAuthentication
方法的最后一步来进行分析。它首先需要获取到一个AuthenticationManager
对象, 这个AuthenticationManager
接口有5个实现类,这里使用的是ProviderManager
类。之后调用ProviderManager
类的authenticate()
方法,因此接下来查看这个authenticate()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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; int currentPosition = 0; int size = this.providers.size(); Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); if (provider.supports(toTest)) { if (logger.isTraceEnabled()) { Log var10000 = logger; String var10002 = provider.getClass().getSimpleName(); ++currentPosition; var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } if (result == null && this.parent != null) { try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException var12) { ; } catch (AuthenticationException var13) { parentException = var13; lastException = var13; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } }
可以看到这个方法中包含的代码非常多,因为几乎所有关于认证的逻辑都在这个方法中。接下来开始分析这个方法:(1) 调用authentication.getClass()
方法来获取当前的Class信息:
1 Class<? extends Authentication> toTest = authentication.getClass();
(2) 通过getProviders().iterator()
方法来得到一个迭代器对象,其类型都是AuthenticationProvider
:
1 2 3 4 5 6 Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); ...... }
(3) 判断当前provider对象是否支持(1)中authentication对象。如果支持,则调用provider的 authenticate()
方法开始认证校验,校验完成后会返回一个新的 Authentication
对象:
1 2 3 4 5 6 7 8 9 if (provider.supports(toTest)) { ...... try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } }
(4) 当(3)中返回的对象不为空,则调用copyDetails()
方法将旧AbstractAuthenticationToken
对象中的details属性复制到新的AbstractAuthenticationToken
对象中:
1 2 3 4 5 6 private void copyDetails(Authentication source, Authentication dest) { if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) { AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest; token.setDetails(source.getDetails()); } }
(5) 当(3)中返回的对象为空,且AuthenticationManager
对象不为空,则调用AuthenticationManager
对象的authenticate()
方法来进行认证校验。(6) 之后调用eraseCredentials
方法将凭证信息擦除掉,这里的凭证信息就是用户密码:
1 2 3 4 5 6 7 8 9 if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; }
它首先判断(3)中的返回对象不为空,且对象是CredentialsContainer
的实例,为什么需要判断?那是因为只有这个CredentialsContainer
接口中才存在擦除凭证信息的eraseCredentials()
方法:
1 2 3 public interface CredentialsContainer { void eraseCredentials(); }
但是这是一个接口,那么接下看一下它的实现类,可以发现一个比较熟悉的身影UsernamePasswordAuthenticationToken
,这里面的eraseCredentials()
方法进行了实现:
1 2 3 4 public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; }
可以看到它擦除凭证的逻辑非常简单,就是将UsernamePasswordAuthenticationToken
对象中的credentials
属性设置为null,其实就是密码设置为空。(7) 通过调用MessageSourceAccessor
对象的publishAuthenticationSuccess()
方法将(3)中成功返回的对象广播出去:
1 2 3 4 5 6 7 8 9 if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; }
以上就是认证的整个流程,第一遍看的时候非常容易晕,这是正常现象,第一次看源码很正常,看多了就习惯了。
这里再捋一下上面的逻辑,首先得到当前Authentication对象的Class信息,接着通过getProviders().iterator()
方法得到一个迭代器对象,之后遍历这个迭代器对象,请注意不是所有的AuthenticationProvider
对象都支持Authentication
,如AnonymousAuthenticationProvider
就不支持,因此它调用AuthenticationManager
接口对象(代码中的parent)的authenticate()
方法进行认证校验。而这个AuthenticationManager
接口对象的实现类是ProviderManager
,因此又回到了此处的authenticate
方法中。不过此时的provider就变成了DaoAuthenticationProvider
,这个provider是支持Authentication
的(如UsernamePasswordAuthenticationToken
),因此会进入到DaoAuthenticationProvider
类的authenticate
方法中执行,但是笔者并没有在DaoAuthenticationProvider
类中发现authenticate
方法:
老规矩,它没有就去查看它实现的接口或者继承的类,可以发现在它继承的AbstractUserDetailsAuthenticationProvider
类中就发现了authenticate
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw var6; } throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); }
仔细查看上述代码的逻辑:(1) 判断authentication
对象是否是UsernamePasswordAuthenticationToken
类型的实例,如果是的话则从Authentication中获取当前登录用户的用户名;(2) 从缓存中通过用户名来获取用户信息,如果获取不到用户信息则说明缓存未启用;反之得到用户信息;(3) 调用retrieveUser方法,将得到的用户名和之前的UsernamePasswordAuthenticationToken
对象作为参数传入,查看一下这个retrieveUser方法的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } }
可以看到它首先调用getUserDetailsService()
方法得到一个UserDetailsService
对象,之后调用它的loadUserByUsername()
方法来获取UserDetails
对象,注意到了么,这个就是之前我们自定义的UserDetailService
类中的loadUserByUsername()
方法,也就是说此处返回的UserDetails
对象其实就是登录对象User:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class MyUserDetailService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByUsername(username); if(user==null){ throw new UsernameNotFoundException("用户不存在"); } return user; } }
(4) 调用preAuthenticationChecks.check(user)
方法来检查User对象的各个属性是否正常。查看一下这个check方法,发现它存在于UserDetailsChecker
接口中:
1 2 3 public interface UserDetailsChecker { void check(UserDetails var1); }
为了弄清楚这个check方法的执行逻辑,这里需要查看UserDetailsChecker
接口的实现类AccountStatusUserDetailsChecker
,在这个实现类中就有check方法的具体实现逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void check(UserDetails user) { if (!user.isAccountNonLocked()) { this.logger.debug("Failed to authenticate since user account is locked"); throw new LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked")); } else if (!user.isEnabled()) { this.logger.debug("Failed to authenticate since user account is disabled"); throw new DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled")); } else if (!user.isAccountNonExpired()) { this.logger.debug("Failed to authenticate since user account is expired"); throw new AccountExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired")); } else if (!user.isCredentialsNonExpired()) { this.logger.debug("Failed to authenticate since user account credentials have expired"); throw new CredentialsExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired")); } }
可以看到这里主要对用户的账户是否未锁住、是否可用、是否未过期、密码是否过期等进行判断,满足其中任意一个都会抛出异常。(5) 接着调用additionalAuthenticationChecks()
方法来对用户密码进行判断,查看这个方法的源码,如下所示:
1 protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
这是一个抽象方法,得找一个这个抽象类的具体实现才行,又回到了DaoAuthenticationProvider
这个类,可以发现这个类中的additionalAuthenticationChecks
方法就提供了具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
在这个方法中首先会判断用户密码是否为空,如果为空则抛出异常;不为空则得到当前登录用户的密码,之后调用passwordEncoder.matches()
方法将之前获取到的密码与数据库中存储的密码是否一致,不一致则抛出异常。以BCryptPasswordEncoder
类中的matches方法为例,查看一下密码的匹配逻辑,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } else if (encodedPassword != null && encodedPassword.length() != 0) { if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) { this.logger.warn("Encoded password does not look like BCrypt"); return false; } else { return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } } else { this.logger.warn("Empty encoded password"); return false; } }
其中rawPassword是获取到的用户密码,而encodedPassword则是数据库中查询到的密码,密码匹配逻辑其实较为简单。
(6) 回到AbstractUserDetailsAuthenticationProvider
类的authenticate
方法中,密码验证通过后,接下来调用postAuthenticationChecks.check(user)
方法来检查密码是否过期。
(7) 之后判断forcePrincipalAsString属性是否为true,该属性是强制将Principal(用户名)转换为String(字符串)对象。其实在UsernamePasswordAuthenticationFilter
类中就已经将principal(用户名)设置为字符串,但是在默认情况下,当用户登录成功后,这个属性的值就成了当前用户,也就是UserDetails对象。需要说明的是,这个forcePrincipalAsString
属性默认值为false,其实开发者不需要管这个参数,因为没转成字符串,反而更利于后续获取用户信息:
1 2 3 4 5 Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user);
(8) 最后调用createSuccessAuthentication()
方法来构造一个Authentication
对象,准确来说是UsernamePasswordAuthenticationToken
对象:
1 2 3 4 5 6 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); this.logger.debug("Authenticated user"); return result; }
以上就是用户的登录流程和认证过程,但是我们依旧不知道登录的用户信息是在哪个环节被保存的,只有搞清楚这个才算真的搞清楚了用户登录。
保存用户信息 我们重新捋一下思路,想想上面的登录是从哪来开始执行的,或者说是触发的?
回到UsernamePasswordAuthenticationFilter
类,这个类非常重要,登录流程就是从这开始的,查看一下它的父类AbstractAuthenticationProcessingFilter
,请注意这个类以后会经常使用,当开发者需要在SpringSecurity中自定义一个登录验证码或者将登录参数修改为JSON的时候,此时都需要自定义自己的Filter类,并继承这个AbstractAuthenticationProcessingFilter
类。
如果你之前使用过Servlet,那么肯定对Filter不陌生,在需要对某些资源进行过滤拦截并进行处理,之后返回处理过的内容,就需要自定义Filter类并实现Filter接口,可以点击 这里 进行了解。这个接口中有一个doFilter方法,当拦截到需要执行的请求时,doFilter方法就会执行,开发者可以在这个方法里面书写对请求和响应的预处理逻辑。
在AbstractAuthenticationProcessingFilter
这个抽象类中,我们也发现它也存在名为doFilter的方法,源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { try { Authentication authenticationResult = this.attemptAuthentication(request, response); if (authenticationResult == null) { return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException var5) { this.logger.error("An internal error occurred while trying to authenticate the user.", var5); this.unsuccessfulAuthentication(request, response, var5); } catch (AuthenticationException var6) { this.unsuccessfulAuthentication(request, response, var6); } } }
仔细阅读上述方法的源码,可以发现首先调用requiresAuthentication(request, response)
方法来判断是否需要验证,如果不需要就直接执行chain.doFilter()
方法;如果需要验证,那么会调用attemptAuthentication()
方法,看到没有,原来是在此处调用了UsernamePasswordAuthenticationFilter#attemptAuthentication()
方法;且当用户登录成功时,就调用successfulAuthentication()
方法,失败则调用unsuccessfulAuthentication()
方法。
接下来看一下登录成功时所调用的successfulAuthentication()
方法的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
注意这里面第一行代码SecurityContextHolder.getContext().setAuthentication(authResult);
,它的作用是获取上下文,并将之前的验证结果赋值给Authentication属性,原来成功的登录信息都会被保存在这里,因此当我们需要获取用户的登录信息时,可以直接从SecurityContextHolder.getContext();
中获取,这其实是一个SecurityContext
对象。当然如果想修改用户信息,也只需修改这个SecurityContext
对象。
回到上面的successfulAuthentication
方法中,继续往下读源码,之后会调用rememberMeServices.loginSuccess()
方法,将登录信息写入“记住我”中,这个是当用户使用了“记住我”这个功能才有效。接着判断是否存在事件广播者,如果存在则将用户登录成功的信息发布出去。如果不存在则调用successHandler.onAuthenticationSuccess()
方法,而这个方法就是我们之前在MySecurityConfig类中配置登录成功时的回调方法,原来那个方法是在这里被触发的:
再来看一下登录失败时所调用的unsuccessfulAuthentication()
方法的源码:
1 2 3 4 5 6 7 8 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); this.logger.trace("Failed to process authentication request", failed); this.logger.trace("Cleared SecurityContextHolder"); this.logger.trace("Handling authentication failure"); this.rememberMeServices.loginFail(request, response); this.failureHandler.onAuthenticationFailure(request, response, failed); }
同样注意这里面的第一行代码SecurityContextHolder.clearContext();
,它的作用是获取上下文,并将上下文内容清空。之后调用rememberMeServices.loginFail()
方法来说明“记住我”也是登录失败的。最后调用failureHandler.onAuthenticationFailure()
方法,而这个方法同样也是之前我们在MySecurityConfig类中配置登录失败时的回调方法,原来那个方法是在这里被触发的:
看到这里我们就对登录流程和认证过程有了清晰的认识,也知道了原来数据保存在SecurityContextHolder.getContext();
中,当开发者需要获取和修改登录信息时,只需对该SecurityContext
对象进行操作即可。
ok,那么本篇关于登录流程的详细分析就到此为止,后续学习其他内容。