Authentication
使用SpringSecurity可以在任何地方注入Authentication進而獲取到當前登錄的用戶信息,可謂十分強大。
在Authenticaiton的繼承體系中,實現類UsernamePasswordAuthenticationToken 算是比較常見的一個了,在這個類中存在兩個屬性:principal和credentials,其實分別代表著用戶和密碼。【當然其他的屬性存在于其父類中,如authorities
和details
。】
我們需要對這個對象有一個基本地認識,它保存了用戶的基本信息。用戶在登錄的時候,進行了一系列的操作,將信息存與這個對象中,后續我們使用的時候,就可以輕松地獲取這些信息了。
那么,用戶信息如何存,又是如何取的呢?繼續往下看吧。
登錄流程
一、與認證相關的UsernamePasswordAuthenticationFilter
通過Servlet中的Filter技術進行實現,通過一系列內置的或自定義的安全Filter,實現接口的認證與授權。
比如: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
|
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形式傳遞參數的原因。
1
2
3
4
5
6
7
8
|
@Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(usernameParameter); } |
構造UsernamePasswordAuthenticationToken對象
傳入獲取到的用戶名和密碼,而用戶名對應UPAT對象中的principal屬性,而密碼對應credentials屬性。
1
2
3
4
5
6
7
8
9
10
|
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //UsernamePasswordAuthenticationToken 的構造器 public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super ( null ); this .principal = principal; this .credentials = credentials; setAuthenticated( false ); } |
為details屬性賦值
1
2
3
4
5
6
7
8
9
10
11
12
|
// 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。
1
2
3
4
5
6
|
public WebAuthenticationDetails(HttpServletRequest request) { this .remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession( false ); this .sessionId = (session != null ) ? session.getId() : null ; } |
調用authenticate方法進行校驗
1
|
this .getAuthenticationManager().authenticate(authRequest) |
二、ProviderManager的校驗邏輯
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
|
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將登錄成功的事件進行廣播。
三、AuthenticationProvider的authenticate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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); } |
從Authenticaiton中提取登錄的用戶名。retrieveUser
方法將會調用loadUserByUsername
方法,這里將會返回登錄對象。preAuthenticationChecks.check(user);
校驗user中的各個賬戶狀態屬性是否正常,如賬號是否被禁用,賬戶是否被鎖定,賬戶是否過期等。additionalAuthenticationChecks
用于做密碼比對,密碼加密解密校驗就在這里進行。postAuthenticationChecks.check(user);
用于密碼比對。forcePrincipalAsString
表示是否強制將Authentication中的principal屬性設置為字符串,默認為false,也就是說默認登錄之后獲取的用戶是對象,而不是username。構建新的UsernamePasswordAuthenticationToken
。
用戶信息保存
我們來到UsernamePasswordAuthenticationFilter 的父類AbstractAuthenticationProcessingFilter 中,
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
|
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); } |
關于登錄成功調用的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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輕松得到用戶信息,主要有下面幾種方法:
通過上下文獲取。
1
|
SecurityContextHolder.getContext().getAuthentication(); |
直接在Controller注入Authentication。
1
2
3
4
|
@GetMapping ( "/hr/info" ) public Hr getCurrentHr(Authentication authentication) { return ((Hr) authentication.getPrincipal()); } |
為什么多次請求可以獲取同樣的信息
前面已經談到,SpringSecurity將登錄用戶信息存入SecurityContextHolder 中,本質上,其實是存在ThreadLocal中,為什么這么說呢?
原因在于,SpringSecurity采用了策略模式,在SecurityContextHolder 中定義了三種不同的策略,而如果我們不配置,默認就是MODE_THREADLOCAL
模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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方法。
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
|
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
過濾器中了)。 -
在過濾器鏈走完之后,數據響應給前端之后,finally 中還有一步收尾操作,這一步很關鍵。這里從
SecurityContextHolder
中獲取到SecurityContext
,獲取到之后,會把SecurityContextHolder
清空,然后調用repo.saveContext
方法將獲取到的SecurityContext
存入 session 中。
總結:
每個請求到達服務端的時候,首先從session中找出SecurityContext ,為了本次請求之后都能夠使用,設置到SecurityContextHolder 中。
當請求離開的時候,SecurityContextHolder 會被清空,且SecurityContext 會被放回session中,方便下一個請求來獲取。
資源放行的兩種方式
用戶登錄的流程只有走過濾器鏈,才能夠將信息存入session中,因此我們配置登錄請求的時候需要使用configure(HttpSecurity http),因為這個配置會走過濾器鏈。
1
2
3
|
http.authorizeRequests() .antMatchers( "/hello" ).permitAll() .anyRequest().authenticated() |
而 configure(WebSecurity web)不會走過濾器鏈,適用于靜態資源的放行。
1
2
3
4
|
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/index.html" , "/img/**" , "/fonts/**" , "/favicon.ico" ); } |
到此這篇關于SpringSecurity中的Authentication信息與登錄流程的文章就介紹到這了,更多相關SpringSecurity登錄流程內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/summerday152/p/13636285.html