本篇文章參考於【江南一點雨】的公衆號。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"); }