双Token机制在实际项目中的应用与实践

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

一、什么是双Token机制?

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

Token类型 作用 有效期 推荐存储位置

Access Token 访问受保护资源的凭证 短(1-2小时) 客户端内存(易失)

Refresh Token 获取新的Access Token 长(7-30天) 安全持久化存储

核心交互流程

text 复制代码
客户端                    服务端
   │ 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 定义

go 复制代码
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,防止中间人攻击

相关推荐
番茄去哪了12 小时前
神领物流面试题(一)
java·大数据·中间件
念何架构之路14 小时前
消息中间件
中间件
都说名字长不会被发现15 小时前
Spring Boot Starter 中间件账号密码加密方案设计与实现
java·spring boot·后端·中间件
瀚高PG实验室1 天前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库
XLYcmy1 天前
一个基于 Python 的轻量级 LLM(大语言模型)API 客户端程序:从API交互到LLM应用架构
服务器·python·ai·llm·prompt·agent·token
之歆2 天前
Day11_Express 深入解析:从中间件到项目实战
中间件·express
码农飞哥2 天前
RocketMQ消费接口设计实战:为什么HTTP回调接口必须吞掉所有异常,始终返回成功?
网络协议·http·中间件·消息队列·rocketmq
硅谷秋水2 天前
物理人工智能的驾驭工程:机器人中间件是驾驭层
人工智能·机器学习·语言模型·中间件·机器人
初中就开始混世的大魔王2 天前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信