在现代Web应用与移动应用中,用户认证是保障系统安全的核心环节。传统的基于Session的单Token机制存在服务端存储压力大、跨域支持困难、移动端适配复杂等痛点。本文结合理论分析与实际项目经验,系统阐述 Access Token + Refresh Token 双令牌机制的设计思想、安全模型与落地要点,帮助读者不仅“会用”,更能“懂用”。

一、什么是双Token机制?

双Token机制是基于JWT(JSON Web Token)的一种认证方案,通过两个不同生命周期的令牌协同工作,兼顾安全性与用户体验。

Token类型 作用 有效期 推荐存储位置
Access Token 访问受保护资源的凭证 短(1-2小时) 客户端内存(易失)
Refresh Token 获取新的Access Token 长(7-30天) 安全持久化存储
核心交互流程

客户端                    服务端
   │ 1. 登录请求           │
   ├─────────────────────→│
   │ 2. 返回双Token        │
   │←─────────────────────┤
   │ 3. 携带Access Token   │
   │   请求API             │
   ├─────────────────────→│
   │ 4. 返回资源           │
   │←─────────────────────┤
   │ 5. Access Token过期   │
   │   携带Refresh Token   │
   │   请求刷新            │
   ├─────────────────────→│
   │ 6. 返回新Token对      │
   │←─────────────────────┤

二、双Token机制的理论基础

2.1 为什么不能只用单Token?

安全与体验的矛盾:若Token有效期短,用户需频繁登录,体验差;若有效期长,一旦泄露,攻击者拥有极长的攻击窗口。

无状态带来的失控:JWT签发后无法主动撤销(除非引入黑名单),用户修改密码后,旧Token依然有效。

无法精细控制权限:不能单独吊销某个设备或某个会话。

2.2 双Token如何解决?

分层安全策略:Access Token作为短期钥匙,即使泄露也很快失效;Refresh Token作为长期凭证,但仅在刷新接口使用,暴露面小。

有状态 + 无状态混合:Access Token保持无状态,便于水平扩展;Refresh Token与Redis会话绑定,实现主动管理和吊销。

用户无感续期:前端拦截401自动刷新,保证用户持续在线。

2.3 与OAuth 2.0的异同

双Token机制借鉴了OAuth 2.0的授权码模式思想,但更轻量:

特性 OAuth 2.0 本项目双Token
适用场景 第三方授权(如“微信登录”) 自主应用用户认证
Token类型 授权码、Access Token、Refresh Token Access Token、Refresh Token
有状态 通常依赖DB Refresh Token有状态
颁发流程 两次交互(授权码换Token) 一次登录即颁发

三、核心数据结构设计

3.1 JWT Claims 定义

type Claims struct {
    UserID    uint   `json:"user_id"`
    Username  string `json:"username"`
    TokenType string `json:"token_type"` // "access" 或 "refresh"
    UserType  string `json:"user_type"`  // "admin" 或 "app"
    Sid       string `json:"sid"`        // 会话ID,关联Redis
    jwt.RegisteredClaims
}

设计原理:

TokenType:防止Refresh Token被误用为Access Token,实现职责分离。

UserType:支持同一系统内多个终端(管理端、普通用户端)使用不同认证策略。

Sid:将JWT与后端会话存储绑定,从而获得主动吊销、多端管理的能力。

3.2 会话存储模型(Redis)

管理端与应用端采用不同策略:

管理端:每个会话独立存储,刷新时轮换Sid,增强防重放能力。

应用端:支持多设备登录限制,使用Set维护用户所有会话ID,可踢出最早登录设备。

这种设计兼顾了不同场景的需求——管理端要求高安全性,应用端更关注设备管理灵活性。

四、双Token的生命周期管理

4.1 颁发(登录)

用户登录成功后,服务端同时生成Access Token与Refresh Token,并将会话信息存入Redis。此时Sid随机生成,作为本次登录的唯一标识。

关键决策:是否存储Access Token到服务端?

我们的方案:仅存储Refresh Token对应的会话,Access Token保持完全无状态。

理由:降低Redis存储压力,避免每次请求都查询会话,提升性能。同时通过TokenType和短有效期控制风险。

4.2 验证(API请求)

中间件执行步骤:

提取Authorization: Bearer 。

解析JWT,校验签名和有效期。

确认TokenType == “access”。

(可选)从Redis校验会话是否存在——如果是高安全级别操作,可以额外检查。

一般建议:常规API仅做JWT校验,敏感操作(修改密码、支付等)再查Redis确认会话有效性。

4.3 刷新(无感续期)

刷新流程的核心设计:

客户端收到401后携带Refresh Token调用刷新接口。

服务端解析Refresh Token(允许过期解析,因为Access Token过期时Refresh Token可能也临近过期但未过)。

校验TokenType == “refresh”,并查Redis确认会话存在。

生成新的Token对,并根据策略决定是否轮换Sid。

管理端:轮换新Sid,删除旧会话。

应用端:可保留原Sid,实现多个Access Token共用一个Refresh会话。

为什么不直接延长Access Token?

因为短有效期Access Token是安全基石,延长它会扩大泄露影响。刷新机制的引入使得用户无需感知续期,而攻击者即使拿到过期的Access Token也无法续期(缺少Refresh Token)。

4.4 主动吊销

当用户修改密码、退出登录、账号被禁用时,需要让所有相关Token失效。方法:

删除Redis中对应的会话记录(按Sid或按UserID批量删除)。

下一次任何请求携带该会话的Token时,中间件可检查Redis发现会话不存在,拒绝访问。

这弥补了JWT无法主动失效的短板。

五、安全模型深度分析

5.1 威胁模型与防御策略

威胁 双Token防御措施
Access Token泄露 有效期短(1-2小时),泄露后攻击窗口有限。
Refresh Token泄露 仅用于刷新接口,暴露频率低;可绑定IP/设备指纹;存入安全存储(httpOnly Cookie/Keystore)。
重放攻击 管理端刷新时轮换Sid,使旧Access Token失效。
中间人攻击 强制HTTPS,防止Token在传输中被截获。
会话劫持(Cookie被盗用) 绑定HttpOnly + Secure + SameSite标志,降低XSS和CSRF风险。
无状态Token篡改 JWT使用HS256签名,服务端验证签名,防止内容篡改。

5.2 存储策略抉择

Web端最佳实践:

Access Token:内存(如变量、Vuex/Redux),页面刷新后丢失,需重新登录或使用Refresh Token恢复。这最大限度减少XSS窃取风险。

Refresh Token:httpOnly Cookie,不可被JavaScript读取,同时设置Secure和SameSite=Strict。

移动端最佳实践:

Access Token:内存或安全存储(如Android KeyStore、iOS Keychain)。

Refresh Token:必须存入Keychain/Keystore,防止恶意应用读取。

5.3 并发刷新与防雪崩

当多个请求同时因Access Token过期而返回401时,若不加控制,会导致大量刷新请求涌入。解决方案:

前端设置“正在刷新”标志,后续401请求进入队列等待。

仅第一个401触发刷新,成功后使用新Token重试队列中的所有请求。

刷新失败时,统一踢出到登录页。

六、不同端策略的差异设计

6.1 管理端 vs. 应用端

维度 管理端 应用端(普通用户)
安全等级 极高 中等
多设备登录 不允许或最多1-2台,且互踢 允许3-5台,可选踢出最早设备
Sid轮换策略 每次刷新均轮换,防重放 长期保持不变,简化实现
会话存储 单会话无Set 使用Set管理用户所有会话
超时时间 Refresh Token 较短(如7天) Refresh Token 较长(如30天)

6.2 “记住我”的实现

“记住我”本质上是延长Refresh Token的有效期,同时保持Access Token时长不变。服务端根据登录请求中的remember_me参数,在生成Refresh Token和存储Redis会话时使用不同的TTL。

需要注意的是,真正的“记住我”还应在客户端持久化Refresh Token(例如存入localStorage),但这会降低安全性。更好的做法是仍使用httpOnly Cookie,但延长服务端过期时间。

七、总结

双Token机制不是简单的“加一个Refresh Token”,而是一套关于安全、体验、架构的平衡艺术。

核心设计原则
职责分离:Access Token负责短期授权,Refresh Token负责长期续期。

混合状态:Access Token无状态以提升性能,Refresh Token有状态以支持管理。

最小暴露:Refresh Token仅在刷新接口传输,避免随每个API请求发出。

主动失效:通过Redis会话实现即时吊销能力。

落地检查清单
Access Token有效期 ≤ 2小时

Refresh Token存储在安全区域(httpOnly Cookie / 安全存储)

实现401自动刷新机制,并处理好并发请求

登录、注销、修改密码时能正确创建/删除Redis会话

对管理端实施更强的刷新策略(如Sid轮换)

强制HTTPS,防止中间人攻击

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐