登录认证是 Web 开发的基石。理解它的原理、特点及各种实现方式,是每个开发者都绕不过去的一关。
在过去的两年学习中,我陆续接触过多种登录机制:session-cookie、X-Auth-Token、SSO、OAuth 2.0、JWT 等等。但坦率地说,真正深入理解它们的设计初衷与区别,我也仅仅停留在“能用”的层面。
最近在项目(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-Token、token、auth 等关键词。
最终,除了在 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 通常存储在前端(sessionStorage 或 localStorage) | 会话 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”,而是一套关于状态、上下文、传递介质、浏览器行为的完整体系。
理解它,才能在遇到问题时做到心中有数,而不是盲目地“试错”。如果你也在处理认证相关的问题,希望这篇文章能帮你少走一些弯路。
限于学识浅薄,思考难免有不周之处。文中若有疏漏、不妥之处,恳请各位不吝赐教,哪怕是细微的指正,我都感激不尽。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。