RefreshToken 不能仅靠前端定时刷新,因会漏掉用户闲置后突操作和网络延迟场景;应由后端在每次有效请求中动态续期,并将 RefreshToken 存于 HttpOnly Cookie(SameSite=Strict+Secure)。
为什么 RefreshToken 不能只靠前端定时请求
后端发的 accesstoken 过期后,用户操作卡住、接口返回 401 unauthorized,这是最常见也最容易误判的“无感”断点。单纯让前端每 15 分钟主动调一次 /api/auth/refresh,会漏掉两种关键场景:用户长时间闲置后突然操作、网络延迟导致刷新请求晚于 accesstoken 失效时刻。真正可靠的续期必须由后端在每次有效请求中动态评估是否该换新 accesstoken。
实操建议:
- 把
RefreshToken存在 HttpOnly Cookie 里(SameSite=Strict+Secure),不暴露给 JS,避免 XSS 泄露 - 每次收到带有效
AccessToken的请求时,检查它剩余有效期是否 AccessToken 并在响应头中用Set-Cookie更新RefreshToken(注意旧RefreshToken要立即失效) - 不要在登录成功后一次性下发长期有效的
RefreshToken,而应为每个刷新动作生成新RefreshToken,并绑定设备指纹或 IP 段做基础校验
ValidateToken 里必须校验 jti 和 used 状态
JWT 本身是无状态的,但续期机制一旦引入 RefreshToken,就必须维护“已使用”状态,否则重放攻击就成立了——攻击者截获一次刷新请求,就能无限次换新 AccessToken。
常见错误现象:RefreshToken 被多次复用、用户登出后 Token 仍可用、同账号多端登录互相踢下线失败。
实操建议:
- 数据库表至少包含字段:
Id、Jti(JWT 唯一标识)、UserId、IsUsed、ExpiresAt、CreatedAt - 在
ValidateToken方法中,除了验证签名和过期时间,必须查库确认jti是否存在且IsUsed == false - 验证通过后立刻执行
UPDATE ... SET IsUsed = true,并用SELECT ... FOR UPDATE或乐观锁防止并发重复使用
C# 中用 AddJwtBearer 配置时别忽略 OnTokenValidated
很多人只配了 TokenValidationParameters 就以为万事大吉,结果续期逻辑全写在 Controller 里,导致权限校验和 Token 刷新割裂——比如用户有权限但 AccessToken 刚好过期 2 秒,接口还是返回 401,根本没走到续期逻辑。
性能影响:把续期判断提前到认证中间件阶段,能统一拦截、减少 Controller 层冗余判断;但要注意不能在 OnTokenValidated 里做耗时 DB 查询,否则拖慢所有请求。
实操建议:
- 在
OnTokenValidated回调里只做轻量检查:读取context.Principal.Identity.Name和context.SecurityToken的ValidTo,计算剩余秒数 - 如果剩余 _tokenService.RenewAccessTokenAsync(...))生成新 Token,并通过
context.HttpContext.Response.Headers.Append("X-Access-Token", newToken)返回给前端 - 不要在这里修改
context.Principal或抛异常中断流程,否则会跳过后续授权检查
前端拿不到新 AccessToken?检查 credentials: 'include' 和 CORS 配置
后端明明写了 Set-Cookie 更新 RefreshToken、也通过响应头返回了新 AccessToken,但前端始终还在用旧 Token 发请求——八成是跨域请求没带凭据,或者后端没允许响应头暴露。
典型错误信息:Refused to get unsafe header "X-Access-Token",或 Cookie 根本没写入浏览器。
实操建议:
- 前端 fetch 必须显式设置
credentials: 'include',Axios 则要加withCredentials: true - 后端
Startup.cs或Program.cs的 CORS 配置里,必须包含:.WithExposedHeaders("X-Access-Token")和.AllowCredentials() - 确保 API 响应状态码是 200(不是 201 或 204),否则部分浏览器不会处理
Set-Cookie
最常被忽略的一点:刷新后的 AccessToken 有效期必须短于原 RefreshToken 的剩余时间,否则会出现“越刷越久”的逻辑漏洞;同时,RefreshToken 自身也要设合理上限(比如最多续 7 天),不能无限滚动。











