将 accessToken 和 refreshToken 存储在 cookie 中的最佳方法
针对你在 .NET 9 后端 + Angular 20 前端技术栈中,基于 Cookie 的身份认证安全优化问题,下面分点解答你的疑问并给出行业推荐的实践方案:
一、把访问令牌和刷新令牌合并到一个 Cookie 里是个好主意吗?
不推荐合并存储,核心原因如下:
- 安全属性配置冲突:
refreshToken 必须配置为HttpOnly(禁止前端 JavaScript 读取,避免XSS攻击窃取),而如果你的 accessToken 需要前端读取并在请求头中携带(比如Authorization: Bearer {token}),则不能设置HttpOnly。合并后无法为两个令牌分别配置关键安全属性,会直接降低安全性。 - 有效期与生命周期差异:
accessToken 通常有效期较短(15-30分钟),refreshToken 有效期较长(7-30天)。分离存储可以独立处理刷新逻辑(比如仅刷新 accessToken 时,不需要更新 refreshToken 的 Cookie),合并后会导致不必要的 Cookie 重写操作。 - Cookie 大小限制:
浏览器单 Cookie 大小上限为 4KB,若两个令牌(尤其是JWT)长度较长,合并后可能超出限制,导致 Cookie 被截断失效。 - 风险扩大化:
若合并后的 Cookie 被泄露(比如CSRF攻击成功),攻击者会同时获取两个令牌,直接拥有长期的身份权限;分离存储则可以针对性撤销其中一个令牌的权限。
二、使用基于会话的方法(Cookie 中存储会话 ID)是否比直接存储令牌更好?
这两种模式各有优劣,需根据你的系统架构选择:
会话 ID 模式(状态化)
优势:
- 仅在 Cookie 中存储短长度的会话ID,敏感的用户身份数据全部存储在后端(比如Redis、数据库),即使 Cookie 被窃取,攻击者需要额外的会话劫持手段才能获取权限,且后端可以主动失效会话。
- 无需前端处理令牌刷新、过期逻辑,身份状态由后端统一管理,降低前端复杂度。
劣势: - 状态化架构,分布式部署时需要共享会话存储(比如Redis集群),增加运维复杂度。
- 不适合无状态的微服务架构,扩展性受限。
直接存储令牌模式(无状态,如JWT)
优势:
- 无状态设计,后端无需存储会话数据,适合分布式、微服务架构,扩展性强。
- 令牌自身包含身份信息,验证时无需查询数据库,性能更高。
劣势: - 令牌一旦签发无法主动撤销(除非维护令牌黑名单),若令牌泄露,攻击者可使用至过期。
- 需要前端处理令牌刷新、过期重定向等逻辑,增加前端开发复杂度。
选择建议:
如果你的系统是单体应用或小规模集群,会话ID模式更简单安全;如果是分布式微服务架构,推荐无状态的令牌模式。
三、.NET + Angular 技术栈中,Cookie 身份认证的推荐安全模式
结合前后端分离架构的特性,推荐以下分层安全实践:
1. 令牌存储策略(核心)
(1)RefreshToken 存储
- 强制配置:存入独立的 Cookie,设置以下关键属性:
HttpOnly: true:禁止前端JS读取,彻底避免XSS窃取风险Secure: true:仅在HTTPS环境下传输,防止明文泄露SameSite: Strict:严格限制Cookie跨站发送,防范CSRF攻击ExpireTimeSpan:设置为7-30天的长有效期
- .NET 9 配置示例:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie("RefreshTokenCookie", options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.Name = "App.RefreshToken"; options.ExpireTimeSpan = TimeSpan.FromDays(14); options.LoginPath = "/auth/login"; }); - 刷新机制:当accessToken过期时,前端调用后端刷新接口,后端验证refreshToken后,返回新的accessToken,并更新refreshToken(令牌旋转,旧refreshToken失效)。
(2)AccessToken 存储
根据前端是否需要读取令牌,分两种方案:
方案A:前端无需读取(推荐)
存入独立的HttpOnly, Secure, SameSite=StrictCookie,后端通过CookieAuthentication自动验证令牌,前端请求时只需携带Cookie(Angular中设置withCredentials: true)。- 优势:完全避免XSS窃取accessToken的风险
- Angular 全局配置示例(自动携带Cookie):
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable() export class CredentialsInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { return next.handle(req.clone({ withCredentials: true })); } } // 在AppModule的providers中注册拦截器 providers: [ { provide: HTTP_INTERCEPTORS, useClass: CredentialsInterceptor, multi: true } ]
方案B:前端需要读取(比如自定义请求头)
存入非HttpOnly的Cookie,但必须配合以下防护:- 启用内容安全策略(CSP),限制非法脚本执行
- 启用XSS防护(浏览器内置的X-XSS-Protection,或.NET的XSS过滤)
- 尽量缩短accessToken的有效期(15分钟以内)
注意:此方案存在XSS窃取风险,仅在前端必须读取令牌时使用,优先选择方案A。
2. 额外安全加固措施
启用CSRF防护
由于使用Cookie认证,必须启用CSRF防护,防止跨站请求伪造:- .NET 9 配置:
builder.Services.AddAntiforgery(options => { options.Cookie.Name = "XSRF-TOKEN"; options.Cookie.HttpOnly = false; // 允许Angular读取 options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.HeaderName = "X-XSRF-TOKEN"; }); - Angular 会自动读取
XSRF-TOKENCookie,并在POST/PUT/DELETE等请求中携带X-XSRF-TOKEN头,无需额外编码。
- .NET 9 配置:
使用HTTPS强制跳转
在.NET中配置强制HTTPS:app.UseHttpsRedirection(); // 启用HSTS增强HTTPS安全性 builder.Services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(365); });令牌旋转与失效
- refreshToken每次刷新时生成新的令牌,旧令牌立即失效,防止令牌复用。
- 若使用JWT,可维护令牌黑名单(比如Redis存储失效的JWT ID),实现主动令牌撤销。
分布式场景优化
若采用令牌模式,推荐使用Reference Token(而非JWT):- 后端存储令牌的完整身份信息,Cookie中仅存储短的引用ID
- 验证时后端查询存储系统获取令牌内容,可主动撤销令牌
- .NET中可通过IdentityServer或OpenIddict实现Reference Token。
总结
针对你的技术栈,最安全且易维护的模式是:
- 分离存储accessToken与refreshToken到不同Cookie,分别配置对应安全属性
- refreshToken严格使用
HttpOnly, Secure, SameSite=Strict,后端管理刷新与旋转 - accessToken优先使用
HttpOnlyCookie,后端自动验证,配合Angular的withCredentials发送请求 - 启用CSRF防护、HTTPS、短有效期accessToken等加固措施
- 分布式系统优先选择无状态令牌模式(JWT或Reference Token),单体系统可选择会话ID模式。




