動態

詳情 返回 返回

修復登錄警告:SPRING_SECURITY_CONTEXT 未包含 SecurityContext 的原因與解決方案 - 動態 詳情

一、問題背景

在日常開發中,我們通常只關注 ERROR 級別日誌,而會忽略 WARN(警告) 級別的信息。
然而,Spring Security 在登錄時產生的大量警告其實暗示了潛在問題。
最近,在處理分配的一個登錄警告修復任務時,我們發現如下日誌出現:

2025-10-16T21:27:07.411+08:00  WARN 3905 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: 'UsernamePasswordAuthenticationToken [Principal=User(username=13920618851, name=系統管理員, password=$2a$10$lDSKF/m7HWpw3tC2pR496.Yak3EcPw9J7.YZY4yU0SqkXA03NsIK., phone=null, status=1, wechatUser=null, roles=[Role(beSystem=true, name=系統管理員, code=systemAdmin, accesses=[Access(authorities=supplier:page,customer:page,outboundOrder:page,personalCenter,access:update,settlement,system,access:getByUuid,productSku:page,access:getAll,access:save,dashboard,finance,inboundOrder:page, description=所有權限, name=所有權限, code=systemAdmin, beSystem=true)], weight=-2147483648)], authorities=[club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5c109170, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1cf66757, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4e42d403, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@7426a5e1, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1a9e54d6, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@f8a6987, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@17167624, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@552af218, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@27984b6f, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@20e97bfe, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@27fb35e3, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@30e8abd0, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@42dddde4, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1890218a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3e1da267]), Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1890218a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@17167624, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5c109170, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@27984b6f, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3e1da267, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1a9e54d6, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@42dddde4, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@27fb35e3, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@7426a5e1, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@1cf66757, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@30e8abd0, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4e42d403, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@f8a6987, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@20e97bfe, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@552af218]]'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?
2025-10-16T21:27:07.412+08:00 DEBUG 3905 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet        : GET "/saleGauge/todayGauge", parameters={}
2025-10-16T21:27:07.412+08:00 DEBUG 3905 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to club.yunzhi.minicrm.controller.SaleGaugeController#getTodayGauge()
2025-10-16T21:27:07.477+08:00 DEBUG 3905 --- [nio-8080-exec-6] org.hibernate.SQL                        : select sr1_0.id,sr1_0.`created_by_id`,sr1_0.created_time,sr1_0.last_settlement_entry_id,sr1_0.last_stock_transaction_id,sr1_0.previous_reconciliation_id,sr1_0.`updated_by_id`,sr1_0.updated_time,sr1_0.uuid from system_reconciliation sr1_0 where sr1_0.created_time<? order by sr1_0.id desc
2025-10-16T21:27:07.530+08:00  WARN 3905 --- [nio-8080-exec-7] w.c.HttpSessionSecurityContextRepository : SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: 'UsernamePasswordAuthenticationToken [Principal=User(username=13920618851, name=系統管理員, password=$2a$10$lDSKF/m7HWpw3tC2pR496.Yak3EcPw9J7.YZY4yU0SqkXA03NsIK., phone=null, status=1, wechatUser=null, roles=[Role(beSystem=true, name=系統管理員, code=systemAdmin, accesses=[Access(authorities=supplier:page,customer:page,outboundOrder:page,personalCenter,access:update,settlement,system,access:getByUuid,productSku:page,access:getAll,access:save,dashboard,finance,inboundOrder:page, description=所有權限, name=所有權限, code=systemAdmin, beSystem=true)], weight=-2147483648)], authorities=[club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@18bf71da, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@7260a86a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@59a3096e, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@22f5f618, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5f86c989, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@312115d1, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@73058c94, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@6973f4a0, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4dbaeace, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@47813f75, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3c544202, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5916ebef, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3c2ff416, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4bd6b46a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@73e867b6]), Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4dbaeace, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@73e867b6, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@59a3096e, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@73058c94, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@6973f4a0, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5916ebef, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@18bf71da, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@47813f75, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@312115d1, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@5f86c989, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3c544202, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@7260a86a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@4bd6b46a, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@22f5f618, club.yunzhi.minicrm.entity.User$$Lambda/0x0000000120ef81f8@3c2ff416]]'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?
2025-10-16T21:27:07.531+08:00 DEBUG 3905 --- [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet        : GET "/saleGauge/getRecentDaysGauges/7", parameters={}

儘管系統功能表面上正常,但這個警告反映出:Spring Security 的會話安全機制被“錯誤地使用”了。

二、問題定位

2.1 報錯信息解析

將報錯信息簡化一下,提取關鍵信息:

w.c.HttpSessionSecurityContextRepository :
SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: 'UsernamePasswordAuthenticationToken[...]'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class?

我們可以發現警告信息來自:HttpSessionSecurityContextRepository —— Spring Security 負責將認證信息(SecurityContext)與會話(HttpSession)關聯的組件。

日誌的含義可以翻譯為:

HttpSessionSecurityContextRepository:
SPRING_SECURITY_CONTEXT中未包含SecurityContext對象,但發現了'UsernamePasswordAuthenticationToken[...]'內容;你是否正在直接修改HttpSession(應始終通過SecurityContextHolder操作)或使用了為此類保留的HttpSession屬性?

換句話説:

Spring Security 框架期望從Session的 SPRING_SECURITY_CONTEXT 屬性中取出一個 SecurityContext 對象,但它卻發現裏面直接存放了一個 UsernamePasswordAuthenticationToken 對象。

2.2 相關名詞解釋

為了理解問題,我們需要先了解幾個關鍵概念:

名稱 作用
UsernamePasswordAuthenticationToken 代表了一次身份認證請求或一個已認證的主體,就是用户身份的載體,包含用户名、密碼、權限等信息。
SecurityContext 封裝了 Authentication,表示安全上下文。
SecurityContextHolder 管理當前線程的 SecurityContext,是獲取認證信息的統一入口。你應該始終通過它來訪問和修改安全上下文,而不是直接操作Session。
HttpSession HTTP 會話,用於在請求間存儲用户相關的數據,會為每個用户創建一個唯一的Session
SPRING_SECURITY_CONTEXT Spring SecuritySession 中保存 SecurityContext 的屬性名常量。

2.3 當前登錄流程及問題根源分析

讓我們通過登錄流程圖來理解問題的發生位置:
截屏2025-10-16 21.50.50.png
問題本質:存儲了錯誤類型的對象
這通常是因為有人繞過了 SecurityContextHolder,直接像下面這樣寫入了HttpSession

// ❌ 錯誤示範!
// 直接存了Token,而不是存SecurityContext
UsernamePasswordAuthenticationToken authToken = ...;
httpSession.setAttribute("SPRING_SECURITY_CONTEXT", authToken); 

為什麼功能正常但仍有警告?

  • ✅ 功能正常:因為 TokenAuthenticationFilter 正確地從 Session 中取出了Authentication 並設置到 SecurityContextHolder
  • ⚠️ 產生警告:Spring Security 檢測到 Session 中存儲的是直接序列化的Authentication(即 UsernamePasswordAuthenticationToken) 而非 SecurityContext,違反框架約定,導致每次請求處理時都會觸發警告日誌,提示可能存在對 HttpSession 的不當操作。
  • 💥 潛在風險:可能導致內存浪費,Spring Security 可能因找不到 SecurityContext 而創建默認空上下文。

    三、解決方案及代碼實現

    3.1 解決後登錄流程

    按照正確的設計規範,修復後的登錄流程應該如下圖所示:
    截屏2025-10-16 21.22.19.png

    3.2 修復登錄接口

修改前:直接存儲 AuthenticationUsernamePasswordAuthenticationToken

// UserController.java
@PostMapping("login")
@JsonView(LoginJsonView.class)
UserDetails login(@RequestBody Map<String, String> loginRequest,
                  HttpServletRequest request) {
    String username = loginRequest.get("username");
    String password = loginRequest.get("password");

    UsernamePasswordAuthenticationToken authToken =
            new UsernamePasswordAuthenticationToken(username, password);
    Authentication authentication = authenticationManager.authenticate(authToken);

    // 將session與認證信息相關聯 ❌ 問題在這裏:直接將 Authentication 存入 Session
    request.getSession(true).setAttribute("SPRING_SECURITY_CONTEXT", authentication);

    return (UserDetails) authentication.getPrincipal();
}

修改後:正確存儲 SecurityContext

// UserController.java
@PostMapping("login")
@JsonView(LoginJsonView.class)
UserDetails login(@RequestBody Map<String, String> loginRequest,
                  HttpServletRequest request) {
    String username = loginRequest.get("username");
    String password = loginRequest.get("password");

    UsernamePasswordAuthenticationToken authToken =
            new UsernamePasswordAuthenticationToken(username, password);
    Authentication authentication = authenticationManager.authenticate(authToken);

    // 創建 SecurityContext 並設置認證信息 ✅
    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
    securityContext.setAuthentication(authentication);

    // 將 SecurityContext 存入 session ✅
    request.getSession(true).setAttribute("SPRING_SECURITY_CONTEXT", securityContext);

    return (UserDetails) authentication.getPrincipal();
}

3.3 修復認證過濾器

登錄時存儲的是 SecurityContext,過濾器讀取時也必須按相同結構解析。
修改前:錯誤地從 Session 獲取 Authentication

// TokenAuthenticationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain)
        throws ServletException, IOException {

    // 1. 從請求頭獲取 x-auth-token
    // ...
    // 2. 如果token不存在,直接放行(後續Security會處理未認證的情況)
    // ...
    // 3. 根據token查找session
    // ...

    // 4. 從session中獲取關聯的認證信息 ❌ 
    Authentication authentication = session.getAttribute("SPRING_SECURITY_CONTEXT");
    if (authentication == null) {
        filterChain.doFilter(request, response);
        return;
    }

    // 5. 設置認證信息到SecurityContext
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 6. 繼續過濾器鏈
    filterChain.doFilter(request, response);
}

修改後:正確地從 SecurityContext 獲取 Authentication

// TokenAuthenticationFilter.java![截屏2025-10-16 21.21.19.png](/img/bVdmJYB)
@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain)
        throws ServletException, IOException {

    // 1. 從請求頭獲取 x-auth-token
    // ...
    // 2. 如果token不存在,直接放行(後續Security會處理未認證的情況)
    // ...
    // 3. 根據token查找session
    // ...

    // 4. session 中獲取 SecurityContext ✅ 
    SecurityContext securityContext = session.getAttribute("SPRING_SECURITY_CONTEXT");
    if (securityContext == null) {
        filterChain.doFilter(request, response);
        return;
    }

    // 5. 從 SecurityContext 中獲取 Authentication ✅ 
    Authentication authentication = securityContext.getAuthentication();
    if (authentication == null) {
        filterChain.doFilter(request, response);
        return;
    }

    // 6. 設置認證信息到SecurityContext
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 7. 繼續過濾器鏈
    filterChain.doFilter(request, response);
}

3.4 修復後後續請求流程

修復完成後,後續請求的處理流程變得更加規範和清晰,具體過程如下圖所示:
截屏2025-10-16 21.12.18.png

四、補充

為了確認修復效果,我們可以通過 Docker 中的 Redis 客户端界面來驗證數據存儲情況。具體操作:

  1. 如圖點擊 Docker 界面,或者直接訪問 http://localhost:5540/
    截屏2025-10-17 15.57.40.png
  2. 再點擊/創建自己的數據庫
    截屏2025-10-17 15.58.29.png
  3. 當登錄成功後就可以生成 HASH 數據,如下圖所示
    截屏2025-10-16 14.05.32.png

    五、總結與心得

    5.1 修復要點回顧:

    存儲時:AuthenticationSecurityContextSession
    讀取時:SessionSecurityContextAuthentication
    核心原則:始終通過 SecurityContextHolder 來操作安全上下文

    5.2 個人心得

    通過最近時間的學習,明確了自己要學的東西還很多,當時對於Redis存儲Session的流程圖表達不太清晰。在學長的指導下,我學會去追源碼以及查看Redis中的Session數據。
    所以,有些技術細節無法僅通過文檔查詢獲得,必須親自動手實踐——包括代碼調試、源碼閲讀和數據驗證。這樣通常會節省很大時間以及加強自己的實踐動手能力。

Add a new 評論

Some HTML is okay.