本篇文章参考于【江南一点雨】的公众号。html
使用SpringSecurity能够在任何地方注入Authentication进而获取到当前登陆的用户信息,可谓十分强大。前端
在Authenticaiton的继承体系中,实现类UsernamePasswordAuthenticationToken 算是比较常见的一个了,在这个类中存在两个属性:principal和credentials,其实分别表明着用户和密码。【固然其余的属性存在于其父类中,如authorities
和details
。】java
咱们须要对这个对象有一个基本地认识,它保存了用户的基本信息。用户在登陆的时候,进行了一系列的操做,将信息存与这个对象中,后续咱们使用的时候,就能够轻松地获取这些信息了。web
那么,用户信息如何存,又是如何取的呢?继续往下看吧。spring
经过Servlet中的Filter技术进行实现,经过一系列内置的或自定义的安全Filter,实现接口的认证与受权。安全
好比:UsernamePasswordAuthenticationFilter
session
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //获取用户名和密码 String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //构造UsernamePasswordAuthenticationToken对象 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 为details属性赋值 setDetails(request, authRequest); // 调用authenticate方法进行校验 return this.getAuthenticationManager().authenticate(authRequest); }
从request中提取参数,这也是SpringSecurity默认的表单登陆须要经过key/value形式传递参数的缘由。app
@Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); }
传入获取到的用户名和密码,而用户名对应UPAT对象中的principal属性,而密码对应credentials属性。ide
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //UsernamePasswordAuthenticationToken 的构造器 public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); }
// Allow subclasses to set the "details" property 容许子类去设置这个属性 setDetails(request, authRequest); protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } //AbstractAuthenticationToken 是UsernamePasswordAuthenticationToken的父类 public void setDetails(Object details) { this.details = details; }
details属性存在于父类之中,主要描述两个信息,一个是remoteAddress 和sessionId。post
public WebAuthenticationDetails(HttpServletRequest request) { this.remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession(false); this.sessionId = (session != null) ? session.getId() : null; }
this.getAuthenticationManager().authenticate(authRequest)
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(); for (AuthenticationProvider provider : getProviders()) { //获取Class,判断当前provider是否支持该authentication if (!provider.supports(toTest)) { continue; } //若是支持,则调用provider的authenticate方法开始校验 result = provider.authenticate(authentication); //将旧的token的details属性拷贝到新的token中。 if (result != null) { copyDetails(authentication, result); break; } } //若是上一步的结果为null,调用provider的parent的authenticate方法继续校验。 if (result == null && parent != null) { result = parentResult = parent.authenticate(authentication); } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { //调用eraseCredentials方法擦除凭证信息 ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null) { //publishAuthenticationSuccess将登陆成功的事件进行广播。 eventPublisher.publishAuthenticationSuccess(result); } return result; } }
获取Class,判断当前provider是否支持该authentication。
若是支持,则调用provider的authenticate方法开始校验,校验完成以后,返回一个新的Authentication。
将旧的token的details属性拷贝到新的token中。
若是上一步的结果为null,调用provider的parent的authenticate方法继续校验。
调用eraseCredentials方法擦除凭证信息,也就是密码,具体来讲就是让credentials为空。
publishAuthenticationSuccess将登陆成功的事件进行广播。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //从Authenticaiton中提取登陆的用户名。 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //返回登陆对象 user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication); //校验user中的各个帐户状态属性是否正常 preAuthenticationChecks.check(user); //密码比对 additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); //密码比对 postAuthenticationChecks.check(user); Object principalToReturn = user; //表示是否强制将Authentication中的principal属性设置为字符串 if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //构建新的UsernamePasswordAuthenticationToken return createSuccessAuthentication(principalToReturn, authentication, user); }
retrieveUser
方法将会调用loadUserByUsername
方法,这里将会返回登陆对象。preAuthenticationChecks.check(user);
校验user中的各个帐户状态属性是否正常,如帐号是否被禁用,帐户是否被锁定,帐户是否过时等。additionalAuthenticationChecks
用于作密码比对,密码加密解密校验就在这里进行。postAuthenticationChecks.check(user);
用于密码比对。forcePrincipalAsString
表示是否强制将Authentication中的principal属性设置为字符串,默认为false,也就是说默认登陆以后获取的用户是对象,而不是username。UsernamePasswordAuthenticationToken
。咱们来到UsernamePasswordAuthenticationFilter 的父类AbstractAuthenticationProcessingFilter 中,
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; Authentication authResult; try { //实际触发了上面提到的attemptAuthentication方法 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } sessionStrategy.onAuthentication(authResult, request, response); } //登陆失败 catch (InternalAuthenticationServiceException failed) { unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { unsuccessfulAuthentication(request, response, failed); return; } if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //登陆成功 successfulAuthentication(request, response, chain, authResult); }
关于登陆成功调用的方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //将登录成功的用户信息存储在SecurityContextHolder.getContext()中 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().getAuthentication()
便可。SecurityContextHolder.getContext().setAuthentication(authResult);
便可。前面说到,咱们能够利用Authenticaiton轻松获得用户信息,主要有下面几种方法:
SecurityContextHolder.getContext().getAuthentication();
@GetMapping("/hr/info") public Hr getCurrentHr(Authentication authentication) { return ((Hr) authentication.getPrincipal()); }
前面已经谈到,SpringSecurity将登陆用户信息存入SecurityContextHolder 中,本质上,实际上是存在ThreadLocal中,为何这么说呢?
缘由在于,SpringSecurity采用了策略模式,在SecurityContextHolder 中定义了三种不一样的策略,而若是咱们不配置,默认就是MODE_THREADLOCAL
模式。
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; public static final String SYSTEM_PROPERTY = "spring.security.strategy"; private static String strategyName = System.getProperty(SYSTEM_PROPERTY); private static void initialize() { if (!StringUtils.hasText(strategyName)) { // Set default strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { strategy = new ThreadLocalSecurityContextHolderStrategy(); } } private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
了解这个以后,又有一个问题抛出:ThreadLocal可以保证同一线程的数据是一份,那进进出出以后,线程更改,又如何保证登陆的信息是正确的呢。
这里就要说到一个比较重要的过滤器:SecurityContextPersistenceFilter
,它的优先级很高,仅次于WebAsyncManagerIntegrationFilter
。也就是说,在进入后面的过滤器以前,将会先来到这个类的doFilter方法。
public class SecurityContextPersistenceFilter extends GenericFilterBean { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null) { // 确保这个过滤器只应对一个请求 chain.doFilter(request, response); return; } //分岔路口以后,表示应对多个请求 HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); //用户信息在 session 中保存的 value。 SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { //将当前用户信息存入上下文 SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { //收尾工做,获取SecurityContext SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); //清空SecurityContext SecurityContextHolder.clearContext(); //从新存进session中 repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); } } }
SecurityContextPersistenceFilter
继承自 GenericFilterBean
,而 GenericFilterBean
则是 Filter 的实现,因此 SecurityContextPersistenceFilter
做为一个过滤器,它里边最重要的方法就是 doFilter
了。doFilter
方法中,它首先会从 repo 中读取一个 SecurityContext
出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository
,读取 SecurityContext
的操做会进入到 readSecurityContextFromSession(httpSession)
方法中。Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
,这里的 springSecurityContextKey
对象的值就是 SPRING_SECURITY_CONTEXT
,读取出来的对象最终会被转为一个 SecurityContext
对象。SecurityContext
是一个接口,它有一个惟一的实现类 SecurityContextImpl
,这个实现类其实就是用户信息在 session 中保存的 value。SecurityContext
以后,经过 SecurityContextHolder.setContext
方法将这个 SecurityContext
设置到 ThreadLocal
中去,这样,在当前请求中,Spring Security 的后续操做,咱们均可以直接从 SecurityContextHolder
中获取到用户信息了。 chain.doFilter
让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter
过滤器中了)。SecurityContextHolder
中获取到 SecurityContext
,获取到以后,会把 SecurityContextHolder
清空,而后调用 repo.saveContext
方法将获取到的 SecurityContext
存入 session 中。总结:
每一个请求到达服务端的时候,首先从session中找出SecurityContext ,为了本次请求以后都可以使用,设置到SecurityContextHolder 中。
当请求离开的时候,SecurityContextHolder 会被清空,且SecurityContext 会被放回session中,方便下一个请求来获取。
用户登陆的流程只有走过滤器链,才可以将信息存入session中,所以咱们配置登陆请求的时候须要使用configure(HttpSecurity http),由于这个配置会走过滤器链。
http.authorizeRequests() .antMatchers("/hello").permitAll() .anyRequest().authenticated()
而 configure(WebSecurity web)不会走过滤器链,适用于静态资源的放行。
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/index.html","/img/**","/fonts/**","/favicon.ico"); }