你在服务端的安全管理使用了 Spring Security,用户登录成功之后,Spring Security 帮你把用户信息保存在 Session 里,但是具体保存在哪里,要是不深究你可能就不知道, 这带来了一个问题,如果用户在前端操作修改了当前用户信息,在不重新登录的情况下,如何获取到最新的用户信息?
玩过 Spring Security 的小伙伴都知道,在 Spring Security 中有一个非常重要的对象叫做 Authentication,我们可以在任何地方注入 Authentication 进而获取到当前登录用户信息,Authentication 本身是一个接口,它有很多实现类:
在这众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken 了,但是当我们打开这个类的源码后,却发现这个类平平无奇,他只有两个属性、两个构造方法以及若干个 get/set 方法;当然,他还有更多属性在它的父类上。
但是从它仅有的这两个属性中,我们也能大致看出,这个类就保存了我们登录用户的基本信息。那么我们的登录信息是如何存到这两个对象中的?这就要来梳理一下登录流程了。
在 Spring Security 中,认证与授权的相关校验都是在一系列的过滤器链中完成的,在这一系列的过滤器链中,和认证相关的过滤器就是 UsernamePasswordAuthenticationFilter::
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //默认的用户名和密码对应的key 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"; //默认只能是post请求 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); //设置details---deltails里面默认存放sessionID和remoteaddr //authRequest 就是构造好的认证令牌 this.setDetails(request, authRequest); //校验 //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; } }
根据这段源码我们可以看出:
UsernamePasswordAuthenticationToken
对象,传入 username 和 password,username
对应了 UsernamePasswordAuthenticationToken
中的 principal
属性,而 password
则对应了它的 credentials
属性。public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 550L; 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; } }
setDetails
方法给 details
属性赋值,UsernamePasswordAuthenticationToken
本身是没有 details
属性的,这个属性在它的父类 AbstractAuthenticationToken
中。details
是一个对象,这个对象里边放的是 WebAuthenticationDetails
实例,该实例主要描述了两个信息,请求的 remoteAddress
以及请求的 sessionId
。好了,从这段源码中,大家可以看出来请求的各种信息基本上都找到了自己的位置,找到了位置,这就方便我们未来去获取了。
接下来我们再来看请求的具体校验操作。
在前面的 attemptAuthentication
方法中,该方法的最后一步开始做校验
,校验操作首先要获取到一个 AuthenticationManager
,这里拿到的是 ProviderManager
,所以接下来我们就进入到 ProviderManager
的 authenticate
方法中,当然这个方法也比较长,我这里仅仅摘列出来几个重要的地方:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //获取到主体(用户名)和凭证(密码)组成的一个令牌对象的class类对象 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; //获取所有可用来校验令牌对象的provider数量 int size = this.providers.size(); //获取迭代器 Iterator var9 = this.getProviders().iterator(); //遍历所有provider while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); //判断当前provider是否支持当前令牌对象的校验 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); //校验成功返回一个新的authentication //将原先的主体由用户名换成了userdetails对象 if (result != null) { //拷贝details到新的令牌对象 this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } //认证失败但是 provider 的 parent不为null if (result == null && this.parent != null) { try { //调用 provider 的 parent进行验证--parent就是providerManager 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; } }
这个方法就比较魔幻了,因为几乎关于认证的重要逻辑都将在这里完成:
大致的流程,就是上面这样,在 for 循环中,第一次拿到的 provider 是一个 AnonymousAuthenticationProvider,这个 provider 压根就不支持 UsernamePasswordAuthenticationToken,也就是会直接在 provider.supports 方法中返回 false,结束 for 循环,然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。
而 parent
就是 ProviderManager
,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate
方法中:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); postAuthenticationChecks.check(user); //如果用户没有使用过,将其放进缓存中 if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
首先从 Authentication 提取出登录用户名。
然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法,所以这里返回的 user 其实就是你的登录对象
接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等
好了,那么登录的校验流程现在就基本和大家捋了一遍了。那么接下来还有一个问题,登录的用户信息我们去哪里查找?
要去找登录的用户信息,我们得先来解决一个问题,就是上面我们说了这么多,这一切是从哪里开始被触发的?
我们来到 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 中,这个类我们经常会见到,因为很多时候当我们想要在 Spring Security 自定义一个登录验证码或者将登录参数改为 JSON 的时候,我们都需自定义过滤器继承自 AbstractAuthenticationProcessingFilter ,毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 类的 doFilter 方法中被触发的:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //不需要认证就直接放行 if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { try { //获取认证的结果---null或者新生产的令牌对象 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); } } }
从上面的代码中,我们可以看到,当 attemptAuthentication 方法被调用时,实际上就是触发了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,当登录抛出异常的时候,unsuccessfulAuthentication 方法会被调用,而当登录成功的时候,successfulAuthentication 方法则会被调用,那我们就来看一看 successfulAuthentication 方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //将新生产的令牌对象放入spring security的上下文环境中 SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
在这里有一段很重要的代码,就是 SecurityContextHolder.getContext().setAuthentication(authResult);
,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从 SecurityContextHolder.getContext()
中获取到,想修改,也可以在这里修改。
最后大家还看到有一个 successHandler.onAuthenticationSuccess
,这就是我们在 SecurityConfig 中配置登录成功回调方法,就是在这里被触发的
当认证失败时,会调用登录失败处理器,并清空上下文环境中的对象
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); }