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版本,對(duì)應(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. 簡(jiǎn)單來說 可以認(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ù)庫(kù)查出來生成的
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 。針對(duì)其他情況,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 : 用戶賬號(hào)被disabled
- LockedException : 用戶賬號(hào)被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();
// 父級(jí)認(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)容請(qǐng)搜索腳本之家以前的文章或繼續(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ù)庫(kù)登錄認(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)文章
SpringMVC 域?qū)ο蠊蚕頂?shù)據(jù)的實(shí)現(xiàn)示例
本文主要介紹了SpringMVC 域?qū)ο蠊蚕頂?shù)據(jù)的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
RocketMQ的push消費(fèi)方式實(shí)現(xiàn)示例
這篇文章主要為大家介紹了RocketMQ的push消費(fèi)方式實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-08-08
SpringBoot+RabbitMQ方式收發(fā)消息的實(shí)現(xiàn)示例
這篇文章主要介紹了SpringBoot+RabbitMQ方式收發(fā)消息的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
Java如何將任意類型的Object對(duì)象轉(zhuǎn)換為相應(yīng)的實(shí)體對(duì)象
這篇文章主要介紹了Java如何將任意類型的Object對(duì)象轉(zhuǎn)換為相應(yīng)的實(shí)體對(duì)象問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

