網站首頁 編程語言 正文
前言
在《Spring Security之認證過濾器》中,我們知道認證信息在用戶登錄成功后,會通過SecurityContextRepository保存。而在《Spring Security之Session管理》中,我們知道SessionManagementConfigurer會創建他。但上面兩篇文章都沒有仔細說,今天作為主角,咱就好好說道說道,并聊聊與之相關的SecurityContextHolder。
SecurityContextRepository
從名字,我們就知道負責維護SecurityContext。先來看看他的定義:
public interface SecurityContextRepository {
/**
* 保存安全上下文
*/
void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
/**
* 從倉庫中查詢當前請求是否存在上下文
*/
boolean containsContext(HttpServletRequest request);
/**
* 從提供的請求中獲取SecurityContext。對于未認證的用戶,應返回空SecurityContext,而不是null。
* HttpRequestResponseHolder參數的作用是允許實現者返回二次封裝的request和response,以便訪問特定的request或者response(也可以都返回)。
* 當最后一次調用的時候,會顯式保存SecurityContext,從holder獲取的值將被傳到過濾器鏈條和saveContext方法中。
* 實現類可能希望返回-當發生錯誤或者重定向時,確保上下文已經保存的-SaveContextOnUpdateOrErrorResponseWrapper的子類作為response對象。
* 實現類可能允許傳入原始的request的response來實現顯式保存。
* @deprecated 請使用#loadDeferredContext(HttpServletRequest)方法代替.
*/
@Deprecated
SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
/**
* 延遲從HttpServletRequest加載SecurityContext,在需要的時候才加載。
* 很明顯這個方法是前者的代替者。
*/
default DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
Supplier<SecurityContext> supplier = () -> loadContext(new HttpRequestResponseHolder(request, null));
return new SupplierDeferredSecurityContext(SingletonSupplier.of(supplier),
SecurityContextHolder.getContextHolderStrategy());
}
}
前面聊session的時候,我們也說過,這個組件有兩個實現,一個負責stateless應用,另一個負責stateful應用。我們重點看看后者。
為什么使用Session存儲認證信息
我們知道Session也就是會話,會話可以用于維護用戶當前一系列操作請求的狀態。而我們的認證信息,就是其中的狀態之一。他表示用戶已經登錄,并且是哪個賬號在訪問系統。但是很顯然的是,我們這些信息只能是一個時間段內的,因為我們的賬號可能會退出登錄態,也就是登出。因此,雖然我們可以在后臺數據庫中維持這一狀態,但是隨著用戶的退出,我們保存在數據庫中的狀態,也就不存在任何意義了,浪費空間。
使用Session來存儲這一信息,正好符合訴求。需要使用時,直接讀取,不用時(退出登錄態)自動清除,騰出空間。
但值得注意的是,使用Session需要留意一下,在登錄后,通常我們都會重建Session,這可能會導致一些問題。
HttpSessionSecurityContextRepository
/**
* SecurityContextRepository的一個實現,將安全上下文保存在HttpSession中。
* 默認情況下,會通過loadContext方法(key: #SPRING_SECURITY_CONTEXT_KEY)從HttpSession中查詢獲取SecurityContext。
* 如果不能從HttpSession中獲取到一個有效的SecurityContext,則不管是什么原因,都會調用新的SecurityContextHolder#createEmptyContext()方法創建一個新的SecurityContext,并返回該實例。
*
* 當saveContext方法被調用時,上下文將使用同樣的key進行保存,為如下場景提供服務:
* <ol>
* <li>值變更</li>
* <li>配置好的AuthenticationTrustResolver不認為該內容代表一個匿名用戶</li>
* </ol>
*
* 在標準配置中,即便是真的不存在HttpSession,loadContext方法也不會創建新的HttpSession。當saveContext在web請求即將結束被調用,只有在傳入的SecurityContext不是一個空的SecurityContext實例時,才會創建一個新的HttpSession。這能夠避免不必要的HttpSession的創建,但會自動存儲請求期間對上下文所做的變更。
* 注意:如果SecurityContextPersistenceFilter配置了提前HttpSession,那么session最小化的邏輯將不再生效。
* 如果你使用急切的session創建方式,那么你應該確保這個類的allowSessionCreation屬性設置為true(默認)。
* <p>
* 如果由于某些原因(例如,使用Basic認證方式或者多個類似的客戶端永遠不會出現相同的jsessionid的情況)導致HttpSession沒有被創建,那么應當通過#setAllowSessionCreation(boolean)將allowSessionCreation屬性設置為false。
* 只有你真正需要節省服務器內存時,并確保所有使用SecurityContextHolder的類,都被設計成:在不同的web請求之間都不需要持久化的SecurityContext時,你才能這樣做。
* @since 3.0
*/
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
// 這個方法重寫了,并沒有使用接口上定義的default實現
// 這里省略了loadContext方法,這個方法的實現比較復雜,還整出了個內部類。相比之下,新的方法就簡單好多。
@Override
public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
Supplier<SecurityContext> supplier = () -> readSecurityContextFromSession(request.getSession(false));
return new SupplierDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
// 這個地方是為了兼容老方法:loadContext
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
SaveContextOnUpdateOrErrorResponseWrapper.class);
if (responseWrapper == null) {
// 這個是新邏輯的核心處理方法,無非就是將內容保存到session之中。
saveContextInHttpSession(context, request);
return;
}
responseWrapper.saveContext(context);
}
@Override
public boolean containsContext(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
return session.getAttribute(this.springSecurityContextKey) != null;
}
}
這里展示了核心方法,并且做了必要的注釋。無非就兩種行為:一個是保存:登陸成功后,保存當前認證信息。另一個是查詢當前會話中的上下文,用于登錄之后的后續請求讀取當前認證信息。
顯然,前者是認證過濾器調用的,也就是UsernamePasswordAuthenticationFilter
。之所以這么明確,是因為其他使用token維護登錄態的,通常只能是每個請求都通過token來認證,說到底就是確認token的有效性。如此一來,這個組件也就失去了作用。
SecurityContextPersistenceFilter
那么,我們什么時候需要讀取認證信息?欸,我們不是有個SecurityContextHolder嗎,我們不是直接通過他拿到當前上下文的嗎?欸,是不是在請求到來的時候,從session中讀取出來恢復的?不對啊,SecurityContextHolder不是static的嗎?假如有多個用戶訪問應用的話,那不全亂套了?
別急,實際上到這里,我們就需要介紹一下當前這個過濾器了,他就是負責將SecurityContextRepository中的認證信息讀取出來,并存入ThreadLocal中。而且時機必須是在所有認證過濾器之前,也必須在請求的業務處理之前,否則就是去意義了。
實際上他在SpringSecurity一系列的過濾之中,執行順序排行第三:
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
}
由于他的職責足夠單一,所以也非常簡單:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 確保當前過濾器只執行一次
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
// 配置了強制生成session(確保session一定存在)
HttpSession session = request.getSession();
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 通過SecurityContextRepository加載當前安全上下文
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
// 將SecurityContext保存到SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 清理上下文 —— 與threadLocal有關
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
}
}
是不是就串起來了?就這樣,我們可以很愉快地通過SecurityContextHolder獲取當前用戶信息了。實際上,如果咱使用的是Spring MVC+Spring Security,可以使用【@AuthenticationPrincipal UserDetails currentUser
】就能獲取到當前登錄用戶了。
SecurityContextHolderStrategy
這里還差一點東西沒有清楚,就是SecurityContextHolder是怎么保存安全信息的。由于涉及并發請求訪問,因此,ThreadLocal自然而然進入了我們的實現。
他如下幾種策略:
策略 | 實現 | 描述 |
---|---|---|
SecurityContextHolder#MODE_THREADLOCAL | ThreadLocalSecurityContextHolderStrategy | 使用ThreadLocal,這是默認策略。在子線程中無法訪問。 |
SecurityContextHolder#MODE_INHERITABLETHREADLOCAL | InheritableThreadLocalSecurityContextHolderStrategy | 使用InheritableThreadLocal,這可以在子線程中獲取當前SecurityContext |
SecurityContextHolder#MODE_GLOBAL | GlobalSecurityContextHolderStrategy | 使用SecurityContextImpl,整個JVM內共享,在特定場景才有用。一般不用他 |
如果真的需要修改策略配置的話,可以直接通過@Bean提供一個實現即可。
public class SecurityContextHolder {
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
}
總結
- 我們通常將當前用戶信息保存在session中,由HttpSessionSecurityContextRepository來管理。
- 在請求進入后,先通過SecurityContextPersistenceFilter將HttpSessionSecurityContextRepository中的SecurityContext恢復到SecurityContextHolder,這樣后續的處理就能通過它來獲取SecurityContext了。業務代碼也無需關注HttpSessionSecurityContextRepository了。
- 默認的SecurityContextHolder策略是ThreadLocal,是不能在子線程中獲取SecurityContext的。如果子線程需要,那么需要配置成InheritableThreadLocal的。
后記
今天,咱們聊了認證信息的處理。并深入了解了兩個組件:SecurityContextRepository、SecurityContextHolderStrategy。這兩個組件實際上是共享組件,涉及到了功能的協同。試想一下,如果SecurityContextPersistenceFilter和認證過濾器使用的不是同一個SecurityContextRepository對象,根本就玩不轉。
下次,咱們再聊另外一個需要協同的功能:認證異常處理。
原文鏈接:https://blog.csdn.net/Evan_L/article/details/138046880
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2022-11-15 Rust使用kind進行異常處理(錯誤的分類與傳遞)_相關技巧
- 2022-08-11 關于pyqtSignal的基本使用_python
- 2022-08-28 本周遇到的一些問題整理,有些未完全解決,留在下周寫
- 2022-12-27 golang時間/時間戳的獲取與轉換實例代碼_Golang
- 2022-07-30 Python實現多腳本處理定時運行_python
- 2022-08-12 Qt實現拖動單個控件移動的示例代碼_C 語言
- 2023-01-01 詳解Python如何實現輸出顏色字體到終端界面_python
- 2022-03-31 C#判斷語句的表達式樹實現_C#教程
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支