Spring Security認(rèn)證機(jī)制源碼層探究
Spring Security提供如下幾種認(rèn)證機(jī)制
- Username & Password
- OAuth2.0 Login
- SAML 2.0 Login
- Remember Me
- JAAS Authentication
- Pre-authentication Scenarios
- X509 Authentication
這里使用Spring Boot 2.7.4版本,對應(yīng)Spring Security 5.7.3版本
Servlet Authentication Architecture
首先明確兩個(gè)概念:
- Principle : This interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, and a login id. 簡單來說 可以認(rèn)為是 唯一確定用戶的 一個(gè) userId ,但這個(gè)Principle是一個(gè)接口,具體參考 java.security.Principal
- Credential : 通常就是一個(gè)密碼,但他不是接口,而是通過接口Authentication#getCredentials來獲取,返回一個(gè)Object類型。
Spring Security提供了以下幾個(gè)核心類:
- SecurityContextHolder : Spring Security用來保存被認(rèn)證用戶的詳細(xì)信息(默認(rèn)使用ThreadLocal保存);
- SecurityContext : 從 SecurityContextHolder 獲取得到,該接口提供被認(rèn)證用戶的 Authentication信息;
- Authentication : 代表"認(rèn)證",其中包含Principle和Credential,通過AuthenticationManager#authenticate來認(rèn)證一個(gè)Authentication,該方法接受一個(gè)未認(rèn)證的Authentication并返回一個(gè)認(rèn)證后的Authentication。
- GrantedAuthority : 認(rèn)證后的Principle包含的權(quán)限
- AuthenticationManager : 實(shí)施認(rèn)證行為的接口,該類為函數(shù)式接口,只有一個(gè)方法: Authentication authenticate(Authentication authentication) throws AuthenticationException;
- ProviderManager : the most common implementation of AuthenticationManager.
- AuthenticationProvider : 提供認(rèn)證方式的接口,組合在ProviderManager中。
SecurityContextHolder
先來看一個(gè)使用SecurityContextHolder和SecurityContext完成認(rèn)證的案例:
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 手動(dòng)生成一個(gè)Authentication,實(shí)際一般是通過數(shù)據(jù)庫查出來生成的 Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER"); context.setAuthentication(authentication); SecurityContextHolder.setContext(context);
默認(rèn) 情況下 SecurityContext存儲(chǔ)使用ThreadLocal方式,這個(gè)就是ThreadLocal的一個(gè)用處,避免函數(shù)間參數(shù)傳遞的復(fù)雜性,只要處于一個(gè)線程,就可以直接獲取而不需要通過函數(shù)參數(shù)返回值來傳遞。
注意:在使用ThreadLocal時(shí),由于鍵是其this本身,是一個(gè)弱引用,而值只能是強(qiáng)引用,所以ThreadLocal不用時(shí)需要手動(dòng)clear。而Spring Security中,在 FilterChainProxy中完成這一清除工作,如下:
public class FilterChainProxy extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ... try { ... // 執(zhí)行Spring Security 的 SecurityFilterChain doFilterInternal(request, response, chain); } catch (Exception ex) { ... } finally { // *************** // 清除ThreadLocal // *************** SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } }
但有些情形是用不了ThreadLocal的,F(xiàn)or example, a Swing client might want all threads in a Java Virtual Machine to use the same security context 。針對其他情況,SecurityContextHolder提供了4種模式,當(dāng)然也可以自定義:
- MODE_THREADLOCAL
- MODE_GLOBAL
- MODE_INHERITABLETHREADLOCAL
- MODE_PRE_INITIALIZED
可以通過2種方式去修改模式:
- set a system property
- a static method on SecurityContextHolder
public class SecurityContextHolder { // 存儲(chǔ)SecurityContext的模式,默認(rèn) ThreadLocal存儲(chǔ) public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED"; public static final String SYSTEM_PROPERTY = "spring.security.strategy"; // 通過系統(tǒng)參數(shù)指定 存儲(chǔ)模式 private static String strategyName = System.getProperty(SYSTEM_PROPERTY); // 實(shí)際存儲(chǔ)SecurityContext的接口(類) private static SecurityContextHolderStrategy strategy; private static int initializeCount = 0; static { // 初始化 initialize(); } private static void initialize() { initializeStrategy(); initializeCount++; } private static void initializeStrategy() { ... if (!StringUtils.hasText(strategyName)) { // Set default 默認(rèn) ThreadLocal strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { strategy = new ThreadLocalSecurityContextHolderStrategy(); return; } ... } // 修改Strategy public static void setStrategyName(String strategyName) { SecurityContextHolder.strategyName = strategyName; initialize(); } // 自定義Strategy public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) { Assert.notNull(strategy, "securityContextHolderStrategy cannot be null"); SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED; SecurityContextHolder.strategy = strategy; initialize(); } public static SecurityContext getContext() { return strategy.getContext(); } public static void setContext(SecurityContext context) { strategy.setContext(context); } public static void clearContext() { strategy.clearContext(); }
可以看到,SecurityContextHolder實(shí)際上是一個(gè)門面,具體的Context存儲(chǔ)在SecurityContextHolderStrategy中,來看看該接口默認(rèn)的實(shí)現(xiàn) ThreadLocalSecurityContextHolderStrategy :
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { // ThreadLocal private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); @Override public void clearContext() { contextHolder.remove(); } @Override public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; } @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder.set(context); } @Override public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
AuthenticationManager
public interface AuthenticationManager { // 傳入一個(gè)待認(rèn)證的Authentication // 返回 a fully authenticated object including credentials Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AuthenticationManager的最常用實(shí)現(xiàn)類 ProviderManager,ProviderManager調(diào)用多個(gè)AuthenticationProvider來認(rèn)證傳入的Authentication,只要有一個(gè)認(rèn)證成功即可返回(返回nonNull),否則會(huì)拋出異常,AuthenticationManager默認(rèn)支持三種異常:
- DisabledException : 用戶賬號被disabled
- LockedException : 用戶賬號被locked
- BadCredentialsException : credentials錯(cuò)誤(密碼錯(cuò)誤)
/** * ProviderManager中的List<AuthenticationProvider>會(huì)按順序認(rèn)證,知道有一個(gè)返回非空。 * 如果后面的 AuthenticationProvider返回非空認(rèn)證結(jié)果,前面拋出的異常統(tǒng)統(tǒng)清除;如果后面還有異常,以第一個(gè)異常為準(zhǔn) * * 該類中有一個(gè) parent 的字段,類型也為AuthenticationManager, * 如果該類中的List<AuthenticationProvider>都不能認(rèn)證,會(huì)調(diào)用parent認(rèn)證,這個(gè)不常用。 * * 事件發(fā)布: * ProviderManager中認(rèn)證事件發(fā)布委托給 AuthenticationEventPublisher 實(shí)現(xiàn),默認(rèn)是空實(shí)現(xiàn)。 * parent 的 ProviderManager中不要實(shí)現(xiàn) Publisher,否則會(huì)重復(fù)發(fā)布。 **/ public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { // 事件發(fā)布,默認(rèn)空實(shí)現(xiàn) private AuthenticationEventPublisher eventPublisher = new NullEventPublisher(); // 實(shí)際認(rèn)證的AuthenticationProvider private List<AuthenticationProvider> providers = Collections.emptyList(); // 父級認(rèn)證Manager private AuthenticationManager parent; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } ... result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } ... } // 該類中AuthenticationManager都判斷完了,結(jié)果還是空,調(diào)用parent開始認(rèn)證 if (result == null && this.parent != null) { // Allow the parent to try. parentResult = this.parent.authenticate(authentication); } ... // AbstractAuthenticationToken就是Authentication接口的實(shí)現(xiàn)類,模板模式 // 常用的UsernamePasswordAuthenticationToken和OAuth2AuthenticationToken都extends這個(gè)Abstract類 private void copyDetails(Authentication source, Authentication dest) { if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) { AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest; token.setDetails(source.getDetails()); } } }
可以看到 ProviderManager implements AuthenticationManager將認(rèn)證工作進(jìn)一步分配給 AuthenticationProvider,而這個(gè)AuthenticationProvider會(huì)根據(jù)支持的認(rèn)證方式來認(rèn)證,所以這個(gè)接口除了認(rèn)證方法還有一個(gè)是否支持認(rèn)證的判斷方法:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication);
AuthenticationProvider常見的實(shí)現(xiàn)類有:
- DaoAuthenticationProvider : An AuthenticationProvider implementation that retrieves user details from a UserDetailsService. 用于UsernamePassword認(rèn)證方式
- OAuth2AuthorizationCodeAuthenticationProvider : 在授權(quán)服務(wù)器上認(rèn)證authorization_code,拿著code換accessToken
- OAuth2LoginAuthenticationProvider : 在授權(quán)服務(wù)器上認(rèn)證authorization_code,拿著code換accessToken(這一步委托給上面的 OAuth2AuthorizationCodeAuthenticationProvider執(zhí)行了),此外,還會(huì)拿著accessToken換取UserInfo,屬于上面的加強(qiáng)版,OAuth2.0 Login一般都是調(diào)用這個(gè)Provider。
- OidcAuthorizationCodeAuthenticationProvider : 同上,不過在OAuth2.0基礎(chǔ)上加了OpenID Connect協(xié)議。
到此這篇關(guān)于Spring Security認(rèn)證機(jī)制源碼層探究的文章就介紹到這了,更多相關(guān)Spring Security認(rèn)證機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity?默認(rèn)登錄認(rèn)證的實(shí)現(xiàn)原理解析
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- Spring Security內(nèi)存中認(rèn)證的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn)
- SpringSecurity身份認(rèn)證原理解析
- springsecurity第三方授權(quán)認(rèn)證的項(xiàng)目實(shí)踐
- Spring Security實(shí)現(xiàn)身份認(rèn)證和授權(quán)的示例代碼
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- SpringBoot security安全認(rèn)證登錄的實(shí)現(xiàn)方法
- Spring Security添加二次認(rèn)證的項(xiàng)目實(shí)踐
相關(guān)文章
SpringBoot整合Dubbo框架,實(shí)現(xiàn)RPC服務(wù)遠(yuǎn)程調(diào)用
Dubbo是一款高性能、輕量級的開源Java RPC框架,它提供了三大核心能力:面向接口的遠(yuǎn)程方法調(diào)用,智能容錯(cuò)和負(fù)載均衡,以及服務(wù)自動(dòng)注冊和發(fā)現(xiàn)。今天就來看下SpringBoot整合Dubbo框架的步驟2021-06-06Java使用RedisTemplate如何根據(jù)前綴獲取key列表
這篇文章主要介紹了Java使用RedisTemplate如何根據(jù)前綴獲取key列表,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Spring Cloud 整合Apache-SkyWalking實(shí)現(xiàn)鏈路跟蹤的方法
這篇文章主要介紹了Spring Cloud 整合Apache-SkyWalking鏈路跟蹤的示例代碼,代碼簡單易懂,通過圖文相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06Springboot集成JUnit5優(yōu)雅進(jìn)行單元測試的示例
這篇文章主要介紹了Springboot集成JUnit5優(yōu)雅進(jìn)行單元測試的示例,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-10-10Mybatis自定義SQL的關(guān)系映射、分頁、排序功能的實(shí)現(xiàn)
這篇文章主要介紹了Mybatis自定義SQL的關(guān)系映射、分頁、排序功能的實(shí)現(xiàn),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01解決org.apache.ibatis.binding.BindingException:?Invalid?boun
這篇文章主要介紹了解決org.apache.ibatis.binding.BindingException:?Invalid?bound?statement?(not?found)問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05Java使用RandomAccessFile類對文件進(jìn)行讀寫
本篇文章主要介紹了Java使用RandomAccessFile類對文件進(jìn)行讀寫,詳細(xì)的介紹了RandomAccessFile類的使用技巧和實(shí)例應(yīng)用,有興趣的可以了解一下2017-04-04手把手教你使用IDEA創(chuàng)建多模塊(maven)項(xiàng)目
這篇文章主要給大家介紹了關(guān)于如何使用IDEA創(chuàng)建多模塊(maven)項(xiàng)目的相關(guān)資料,文中通過圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07