博客 / 詳情

返回

登錄認證:從 X-Auth-Token 到 Cookie 的一次實踐與反思

登錄認證是 Web 開發的基石。理解它的原理、特點及各種實現方式,是每個開發者都繞不過去的一關。
在過去的兩年學習中,我陸續接觸過多種登錄機制:session-cookieX-Auth-TokenSSOOAuth 2.0JWT 等等。但坦率地説,真正深入理解它們的設計初衷與區別,我也僅僅停留在“能用”的層面。

最近在項目(Angular 18.2.0 + Spring Boot 3.2.3)中遇到一個實際問題:登錄認證需要從基於請求頭(header)中的 X-Auth-Token 改為基於 Cookie 的認證方式。
這次問題讓我下定決心,徹底理清它們的本質差異,於是有了這篇記錄。


一、問題起點:前端似乎“沒有 X-Auth-Token”

初看問題時,我自然從前端入手。於是依次閲讀了:

  • 登錄組件 login.component.ts
  • 用户服務 user.service.ts
  • 全局攔截器 interceptor

結果卻令人費解:幾乎沒看到任何關於 X-Auth-Token 的邏輯。
我懷疑自己遺漏了什麼,於是全局搜索 X-Auth-Tokentokenauth 等關鍵詞。
最終,除了在 user.api 文件中發現以下一行代碼,其他地方都一片空白:

const xAuthToken = options.headers.get('x-auth-token'); // user.api

這讓我意識到,也許問題的關鍵並不在前端顯眼的登錄邏輯中,而是隱藏在更深層的認證機制裏。
於是我轉向後端,從請求和配置層面追查。


二、後端的關鍵:HttpSessionIdResolver 與過濾器

首先在後端,我重點查看了以下文件:

  • 用户控制器(UserController
  • 用户服務實現類(UserServiceImpl
  • 與登錄認證相關的過濾器和配置類

不出所料,關鍵邏輯藏在 Web 配置類和 Token 過濾器中。

/**
 * 啓用基於 x-auth-token 的 header 認證。
 * 啓用後,Spring 會使用 x-auth-token 替代 cookie 傳遞 session。
 */
@Bean
HttpSessionIdResolver sessionIdResolver() {
    return HeaderHttpSessionIdResolver.xAuthToken();
}

這行配置就是問題的根源。

眾所周知,HTTP 是無狀態協議,服務器需要某種方式識別用户的會話(Session)。
Spring 的默認做法是通過 Cookie 傳遞 Session ID(通常名為 JSESSIONID),例如:

Cookie: JSESSIONID=ABC123XYZ

HttpSessionIdResolver 接口,正是 Spring 提供的“決定 Session ID 從哪裏讀取、寫到哪裏”的機制。

HeaderHttpSessionIdResolver 的作用,是讓 Spring 從自定義的 HTTP Header(這裏就是 x-auth-token)中讀取與寫入 Session ID。

因此,當我們刪除上面的配置後,Spring 就會退回默認的 CookieHttpSessionIdResolver,也就是基於 Cookie 傳遞 Session ID 的模式。

與此同時,之前為 Header 模式設計的 TokenAuthenticationFilter 也失去了存在意義。


三、過濾器的原理:如何從 Header 恢復認證狀態

過濾器的核心代碼如下:

/**
 * 基於 x-auth-token 的過濾器
 * 用於從請求頭中恢復認證信息
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final RedisSessionRepository sessionRepository;

    public TokenAuthenticationFilter(RedisSessionRepository sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        String token = request.getHeader("x-auth-token");

        if (token == null || token.isBlank()) {
            filterChain.doFilter(request, response);
            return;
        }

        Session session = sessionRepository.findById(token);
        if (session == null) {
            filterChain.doFilter(request, response);
            return;
        }

        SecurityContext securityContext = session.getAttribute("SPRING_SECURITY_CONTEXT");
        if (securityContext == null) {
            filterChain.doFilter(request, response);
            return;
        }

        Authentication authentication = securityContext.getAuthentication();
        if (authentication == null) {
            filterChain.doFilter(request, response);
            return;
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

這段過濾器的邏輯其實十分典型,作用可以概括為一句話:

它讓後端能用請求頭中的 x-auth-token(也就是 session id)去 Redis 查回 Spring Session,從而恢復出用户的認證上下文。

下面逐步拆解關鍵邏輯:

步驟 含義 説明
從請求頭獲取 x-auth-token 即 Header 模式下的 session id
若 token 不存在,放行 交由後續 Spring Security 處理
根據 token 查 Redis 中的 session 若不存在或過期,同樣放行
從 session 取出 SPRING_SECURITY_CONTEXT Spring Security 默認存放認證上下文的鍵
獲取 Authentication 對象 若為空,視為未登錄
將認證信息放入 SecurityContextHolder 使後續請求在當前線程中可獲取用户信息

換句話説,這個過濾器就是在請求鏈最前端“補齊登錄狀態”的關鍵組件。


四、切換到 Cookie 模式後:請求變化分析

完成後端修改(刪除 HeaderHttpSessionIdResolver 配置和TokenAuthenticationFilter過濾器)後,再發起登錄請求。

查看瀏覽器的請求頭:

Cookie: b-user-id=ed296278-22d7-6b44-cc03-a1ecf58f2c61; SESSION=M2I1ODhiMWItYWRjNi00NThkLTk2NDYtNGU2NjYyOTgwMjNi
x-auth-token: c1e3d4e6-e5cd-4677-83f0-2424242b8c6c

這裏可以看到:

  • SESSION=... 是瀏覽器自動攜帶的 Cookie,會話標識由後端生成。
  • x-auth-token=... 依然存在,這顯然是前端主動加上的。

於是問題鎖定在前端。


五、前端的幕後黑手:XAuthTokenInterceptor

字斟句酌,仔細檢查攔截器註冊部分,終於找到了關鍵配置:

export const httpInterceptorProviders: Provider[] = [
  { provide: HTTP_INTERCEPTORS, useClass: ApiPrefixAndMergeMapWrapperInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: NullOrUndefinedOrEmptyInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: Prevent401Popup, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true },
  {
    provide: HTTP_INTERCEPTORS,
    useClass: XAuthTokenInterceptor, // ← 罪魁禍首
    multi: true
  },
  { provide: HTTP_INTERCEPTORS, useClass: HttpErrorWrapperInterceptor, multi: true }
];

Angular 的攔截器(HTTP_INTERCEPTORS)是通過依賴注入機制註冊的。
每個攔截器都可以在請求發送前或響應返回後,插入自定義邏輯。

配置項説明如下:

  • provide: HTTP_INTERCEPTORS → 表示要注入 HTTP 攔截器。
  • useClass: XxxInterceptor → 指定具體攔截器類。
  • multi: true → 允許多個攔截器共存(以數組形式追加,而非覆蓋已有的實例)。

很顯然,XAuthTokenInterceptor 會在每個請求中主動添加 x-auth-token 頭。
在切換到 Cookie 模式後,這一行為已經沒有必要,反而可能導致後端的混亂。


六、總結與反思:兩種認證方式的本質區別

維度 Header 模式(X-Auth-Token) Cookie 模式(JSESSIONID)
會話標識傳遞方式 通過自定義 HTTP Header(如 x-auth-token)手動傳遞 瀏覽器自動攜帶 Cookie(通常為 JSESSIONID
會話存儲位置 Token 通常存儲在前端(sessionStoragelocalStorage 會話 ID 存在瀏覽器 Cookie(HttpOnly,可防 JS 訪問)
前端控制程度 高:前端需顯式讀取、保存、附加 token 低:瀏覽器自動處理 Cookie 發送與過期
跨域兼容性 原生支持好(Header 可跨域) 需配置 CORS 且設置 withCredentials
安全性(XSS/CSRF) 易受 XSS 攻擊(token 可被腳本竊取);幾乎無 CSRF 風險 XSS 安全(HttpOnly 可防 JS 讀 Cookie);有 CSRF 風險
服務端支持情況 需額外配置 HeaderHttpSessionIdResolver 或自定義 Filter Spring 默認支持,無需額外配置
實現複雜度 中等:需前端攔截器 + 後端 Filter 配合 低:Spring Boot 原生支持
無狀態 / 有狀態 可偏向“無狀態”(Header 可脱離 Session) 明確有狀態(基於服務器 Session)
用户登出/會話失效控制 需前端主動清理存儲的 token 後端清理 session 後瀏覽器自動失效
適合誰 前後端完全分離、跨域複雜的 SPA (單頁應用) 基於瀏覽器的傳統 Web 系統(含現代前端框架)

七、補充説明:Cookie 與安全機制

在切換為 Cookie 模式後,兩個細節容易被忽視:withCredentials 的作用XSS / CSRF 的區別

withCredentials 到底有什麼用?

簡單來説,它決定 前端是否在跨域請求中攜帶 Cookie
在常見的跨域架構下(前端 localhost:4200 → 後端 localhost:8080),如果你想讓瀏覽器帶上 Cookie,就必須在請求中顯式聲明:

this.http.get('/api/user/current', { withCredentials: true });

否則瀏覽器會自動剝離 Cookie。
同時,後端也要允許攜帶憑證:

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:4200")
                        .allowCredentials(true);
            }
        };
    }
}

但在本項目中,請求經過 Nginx 反向代理(同域轉發),瀏覽器天然視為同源,因此不需要 withCredentials
換句話説,只要前後端域名一致(哪怕只是代理層面),Cookie 會自動發送,無需任何額外設置。


關於 XSS 與 CSRF —— 一圖讀懂

攻擊類型 目標 原理 防禦思路
XSS(跨站腳本攻擊) 用户瀏覽器 攻擊者在頁面中注入惡意腳本,竊取 Cookie 或操作頁面 使用 HttpOnly Cookie、防輸入注入、嚴格 CSP(內容安全策略)
CSRF(跨站請求偽造) 後端服務器 利用用户已登錄狀態,誘導其發送偽造請求 使用 CSRF Token、SameSite Cookie、防外域表單提交

簡而言之:

  • XSS 是“讓你的瀏覽器做壞事”;
  • CSRF 是“讓你在不知情的情況下,替別人發請求”。

基於 HttpOnly 的 Cookie 模式天然抵禦 XSS 竊取 Token,而防 CSRF 則要靠 SameSite Cookie 策略CSRF Token 校驗


七、寫在最後

這次從 Header 模式改回 Cookie 模式的過程,看似只是“刪掉幾行配置”,但背後牽涉了整個認證機制的運行邏輯。在查詢過程中,受益匪淺,感嘆和感激於編程世界的深奧複雜與潘老師給予學習機會的來之不易,並且堅信:路漫漫其修遠兮,吾將上下而求索。

同時,也讓我重新認識到:
登錄認證不是單純的“存個 token”,而是一套關於狀態、上下文、傳遞介質、瀏覽器行為的完整體系。

理解它,才能在遇到問題時做到心中有數,而不是盲目地“試錯”。如果你也在處理認證相關的問題,希望這篇文章能幫你少走一些彎路。

限於學識淺薄,思考難免有不周之處。文中若有疏漏、不妥之處,懇請各位不吝賜教,哪怕是細微的指正,我都感激不盡。


user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.