Skip to content
Go back

双Token认证原理以及实现的步骤

Published:  at  7:00 AM

双 Token 认证原理与实现步骤

1. 什么是双 Token

双 Token 指同时使用两种令牌:

Token用途常见有效期
Access Token调用普通业务接口较短,例如 15 分钟
Refresh TokenAccess Token 过期后换取新 Token较长,例如 7 天

可以简单理解为:

Access Token = 临时通行证
Refresh Token = 补办临时通行证的凭证

用户调用课程、练习、AI 对话等普通业务接口时,只携带 Access Token。

Refresh Token 不参与普通业务请求,只发送给专门的刷新接口。


2. 为什么需要双 Token

如果项目只使用一个 JWT,就会面临安全性和用户体验之间的矛盾。

Token 有效期较长

优点:用户不需要频繁登录
缺点:Token 泄露后,攻击者可以使用很长时间

Token 有效期较短

优点:Token 泄露后的可利用时间较短
缺点:用户可能每隔十几分钟就要重新登录

双 Token 的解决方式

Access Token 生命周期短,降低泄露风险
Refresh Token 生命周期长,维持用户登录状态

3. 完整工作流程

3.1 用户登录

用户名和密码
→ 后端验证用户
→ 生成 Access Token
→ 生成 Refresh Token

推荐的返回方式:

Access Token
→ 返回给前端
→ 保存在 Zustand 等内存状态中

Refresh Token
→ 通过 Set-Cookie 写入浏览器
→ 使用 HttpOnly Cookie 保存

HttpOnly Cookie 不能被前端 JavaScript 读取,可以降低 XSS 攻击直接窃取 Refresh Token 的风险。

3.2 调用普通接口

例如请求小节内容:

GET /api/v1/chapter/lesson/abc12
Authorization: Bearer ACCESS_TOKEN

后端的认证中间件只验证 Access Token。

验证成功:

返回小节内容

Access Token 已过期:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "code": "TOKEN_EXPIRED",
  "message": "Access Token 已过期"
}

3.3 自动刷新

前端发现接口返回:

HTTP 401
+ TOKEN_EXPIRED

前端调用刷新接口:

POST /api/v1/auth/refresh
Cookie: refreshToken=...

因为 Refresh Token 保存在 Cookie 中,所以浏览器可以自动携带,不需要 JavaScript 读取它。

后端验证 Refresh Token:

Refresh Token 有效
→ 生成新的 Access Token
→ 返回给前端
→ 前端重新发送刚才失败的请求

这个过程通常由请求拦截器自动完成,用户不会明显感觉到。

3.4 Refresh Token 也已过期

如果刷新接口同样返回 401:

清空前端登录状态
→ 删除本地 Access Token
→ 跳转登录页面
→ 用户重新登录

这才是真正的登录失效。


4. 是否需要更换 Refresh Token

4.1 简单方案

每次刷新时,只生成新的 Access Token:

旧 Refresh Token
→ 新 Access Token

优点:

缺点:

4.2 推荐方案:Refresh Token Rotation

每次刷新时,同时更换 Access Token 和 Refresh Token:

旧 Refresh Token
→ 新 Access Token
→ 新 Refresh Token
→ 旧 Refresh Token 立即作废

这个过程叫作 Refresh Token Rotation,即刷新令牌轮换。

如果已经作废的旧 Refresh Token 再次出现,说明它可能已经泄露。后端可以撤销整个登录会话,要求用户重新登录。


5. JWT 为什么还需要 Redis 或数据库

JWT 经常被称为“无状态认证”,但不代表认证系统完全不需要服务端状态。

Access Token 可以采用无状态验证:

后端只验证 JWT 签名和过期时间

Refresh Token 最好建立服务端会话,并在 Redis 或数据库中保存:

会话 ID
用户 ID
Refresh Token 哈希
Token 唯一 ID
过期时间
设备信息
是否已撤销
创建时间
最近刷新时间

不要直接保存 Refresh Token 明文,应该保存它的哈希值,思路类似于密码存储。

CodeStory 已经使用 Redis,后续可以设计类似的键:

refresh_session:{sessionId}

示例数据:

{
  "userId": "user-123",
  "refreshTokenHash": "...",
  "tokenId": "token-456",
  "expiresAt": "2026-06-20T10:00:00.000Z",
  "revoked": false
}

6. 两种 Token 应该包含什么

6.1 Access Token

{
  "sub": "用户 ID",
  "role": 0,
  "sessionId": "登录会话 ID",
  "type": "access",
  "iat": 1000000000,
  "exp": 1000000900
}

建议包含:

6.2 Refresh Token

{
  "sub": "用户 ID",
  "sessionId": "登录会话 ID",
  "tokenId": "Refresh Token 唯一 ID",
  "type": "refresh",
  "iat": 1000000000,
  "exp": 1000604800
}

建议包含:

必须使用 type 区分两种 Token,防止有人使用 Refresh Token 调用普通业务接口。


7. 推荐的前端存储方案

Access Token

保存在 Zustand 等内存状态中

特点:

Refresh Token

保存在 Secure + HttpOnly + SameSite Cookie 中

特点:

不推荐的方案

Access Token 和 Refresh Token 全部存入 localStorage

如果页面发生 XSS,恶意脚本就可能同时读取两个 Token。


推荐配置:

HttpOnly = true
Secure = 生产环境为 true
SameSite = Lax 或 Strict
Path = /api/v1/auth

各配置的作用:

配置作用
HttpOnly禁止 JavaScript 读取 Cookie
Secure只允许通过 HTTPS 发送
SameSite降低跨站请求伪造风险
Path限制 Cookie 只发送给认证相关路径
Max-Age / Expires设置 Refresh Token 有效期

如果前后端跨域,还需要处理:


9. CodeStory 的推荐实现顺序

真正开发时,建议按照以下顺序实施。

第一步:定义认证规则

先确定:

第二步:拆分 Token 工具

分别实现:

生成 Access Token
验证 Access Token
生成 Refresh Token
验证 Refresh Token

不要使用同一个函数和完全相同的配置处理两种 Token。

第三步:建立刷新会话

使用 Redis 或数据库保存:

sessionId
userId
refreshTokenHash
tokenId
expiresAt
revoked

第四步:修改登录接口

登录成功后:

生成 Access Token
生成 Refresh Token
创建刷新会话
通过 JSON 返回 Access Token
通过 HttpOnly Cookie 返回 Refresh Token

第五步:修改认证中间件

普通接口只允许使用 Access Token:

验证签名
验证过期时间
验证 type === "access"
读取用户 ID 和角色

第六步:增加刷新接口

例如:

POST /api/v1/auth/refresh

接口负责:

读取 Refresh Cookie
验证 Refresh Token
查询服务端刷新会话
比较 Refresh Token 哈希
执行 Token Rotation
返回新的 Access Token
设置新的 Refresh Cookie

第七步:修改前端请求拦截器

业务接口返回 Access Token 过期时:

调用刷新接口
→ 获取新 Access Token
→ 更新 Zustand 状态
→ 重新发送原请求

第八步:处理并发刷新

多个请求同时过期时,只允许发送一次刷新请求。

其他请求等待刷新结果,然后统一重试。

第九步:修改退出登录

退出登录时:

撤销 Redis 刷新会话
→ 清除 Refresh Cookie
→ 清空前端 Access Token
→ 清空用户信息

第十步:补充安全测试

至少测试:


10. 前端最容易踩的坑:并发刷新

假设页面同时发出三个请求:

获取用户信息
获取小节内容
获取课程进度

三个请求同时发现 Access Token 已过期。

错误做法

请求 A → 刷新一次
请求 B → 刷新一次
请求 C → 刷新一次

如果使用 Refresh Token Rotation:

请求 A 使用旧 Refresh Token 刷新成功
→ 旧 Refresh Token 立即作废

请求 B 再使用旧 Refresh Token
→ 刷新失败

请求 C 再使用旧 Refresh Token
→ 刷新失败

最终可能导致用户被错误地退出登录。

正确做法

第一个 401
→ 创建唯一的刷新 Promise

其他 401
→ 等待这个 Promise

刷新成功
→ 更新 Access Token
→ 所有失败请求使用新 Token 重新发送

伪代码思路:

let refreshPromise: Promise<string> | null = null;

async function getNewAccessToken() {
  if (!refreshPromise) {
    refreshPromise = refreshAccessToken().finally(() => {
      refreshPromise = null;
    });
  }

  return refreshPromise;
}

这段代码现在不需要照着实现,重点理解:

同一时刻只能存在一个刷新请求。


11. 完整时序图

sequenceDiagram
    participant U as 用户
    participant F as Next.js 前端
    participant B as Express 后端
    participant R as Redis

    U->>F: 登录
    F->>B: 提交账号和密码
    B->>R: 创建刷新会话
    B-->>F: Access Token + Refresh Cookie

    F->>B: 携带 Access Token 请求课程
    B-->>F: 返回课程数据

    F->>B: 携带已过期的 Access Token
    B-->>F: 401 TOKEN_EXPIRED

    F->>B: 请求刷新,自动携带 Refresh Cookie
    B->>R: 验证刷新会话
    B->>R: 轮换 Refresh Token
    B-->>F: 新 Access Token + 新 Refresh Cookie

    F->>B: 使用新 Access Token 重试原请求
    B-->>F: 返回业务数据

12. 常见安全问题

12.1 不区分 Token 类型

错误:

Access Token 和 Refresh Token 使用相同验证逻辑

风险:

攻击者可能使用 Refresh Token 调用普通接口

解决:

Access Token 必须满足 type === "access"
Refresh Token 必须满足 type === "refresh"

12.2 Refresh Token 不可撤销

如果后端不保存刷新会话:

用户退出登录后
→ 已签发的 Refresh Token 仍可能继续刷新

解决:

使用 Redis 或数据库保存刷新会话
退出登录时撤销会话

12.3 在日志中打印 Token

不要记录:

Authorization 请求头
完整 Access Token
完整 Refresh Token
Cookie 原文

12.4 无限自动刷新

如果刷新接口本身返回 401,前端不能再次拦截并继续刷新,否则会形成死循环。

需要明确排除:

/api/v1/auth/login
/api/v1/auth/refresh

12.5 所有 401 都触发刷新

401 不一定代表 Access Token 过期,也可能是:

前端最好只在后端返回明确的 TOKEN_EXPIRED 错误码时刷新。


13. 推荐错误码

HTTP 状态码业务错误码含义
401ACCESS_TOKEN_MISSING未携带 Access Token
401ACCESS_TOKEN_EXPIREDAccess Token 已过期,可以尝试刷新
401ACCESS_TOKEN_INVALIDAccess Token 无效,不应刷新
401REFRESH_TOKEN_MISSING未携带 Refresh Token
401REFRESH_TOKEN_EXPIREDRefresh Token 已过期,需要重新登录
401REFRESH_TOKEN_INVALIDRefresh Token 无效
401SESSION_REVOKED登录会话已被撤销
403FORBIDDEN已登录但没有操作权限

只有:

ACCESS_TOKEN_EXPIRED

应该自动触发刷新。


14. 最需要记住的三个概念

短期 Access Token 负责访问
长期 Refresh Token 负责续期
服务端刷新会话负责撤销和轮换

完整流程可以压缩成:

登录
→ 获得 Access Token 和 Refresh Token
→ Access Token 调用业务接口
→ Access Token 过期
→ Refresh Token 换取新 Token
→ 自动重试原请求
→ Refresh Token 失效后重新登录

15. 学习检查清单

复习完成后,尝试回答以下问题:

如果能够用自己的语言回答这些问题,就已经掌握了双 Token 的核心思路。


Suggest Changes
Share this post on:

Previous Post
Docker部署血泪史
Next Post
如何从0-1写一个vscode插件