jwt 的 token 失效机制
核心概念汇总
- JWT 是什么?
- JWT (JSON Web Token) 是目前最流行的跨域认证解决方案。
- 特点:自包含、无状态、安全性、可扩展、可跨域、可加密。
- 缺点:无法主动失效、无法主动更新。
- JWT 的 token 失效机制:
- 使用 JWT 要非常明确的一点:JWT 失效的唯一途径就是等待时间过期。
- 黑名单机制:将失效的 token 存储在数据库中,每次请求时,检查 token 是否在黑名单中。
- 白名单机制:将有效的 token 存储在数据库中,每次请求时,检查 token 是否在白名单中。
- 不管是记录白名单还是黑名单,都需要一个记录的地方。对于分布式系统,还是需要每次使用前去记录的地方查询对比,和 session 方案没本质区别。
- 最佳实践:
- JWT 失效机制:合理设置过期时间,使用 Refresh Token 延长会话。
- 使用 Refresh Token 延长会话,可以避免频繁刷新 Token,提高用户体验。
- Refresh Token 的过期时间应该比 JWT 的过期时间更长。
- 参考以上【黑/白名单机制】。
- 最小化信息存储:避免存储敏感数据,简化 JWT Payload。
- 服务器的黑名单(或白名单)只存储
jti
,而不是整个 JWT。
- 服务器的黑名单(或白名单)只存储
- 安全存储与传输:使用 HttpOnly Cookie 存储,确保通过 HTTPS 传输。
- 密钥管理与加密算法:使用强加密算法,定期轮换密钥。
- 防止重放攻击:通过
jti
防止 Token 被重放。 - 最小权限原则:控制角色和权限,实施细粒度权限管理。
- 跨服务认证:实现 JWT 跨服务认证,使用 API Gateway 简化验证。
- JWT 失效机制:合理设置过期时间,使用 Refresh Token 延长会话。
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
因为 JWT
是无状态的,所以它的有效期完全由其本身决定,也就是说服务端无法让一个 token
失效。
一、token 失效机制
Token 失效机制是保证系统安全性的重要部分。通过合理的失效设计,可以减少攻击风险,防止被盗 Token 长时间滥用。以下是 Token 失效机制的常见实现方式及其具体补充:
1. 时间失效
通过设置有效期限制 Token 的使用时间。
(1) 短期有效的 Access Token
- 设置较短的有效期(如 5-15 分钟)。
- 优点:即使 Token 被截获,攻击者使用时间受限。
- 实现:
- 在生成 Token 时,添加
exp
(过期时间)。json{ "sub": "1234567890", "name": "John Doe", "exp": 1714656000 // Unix 时间戳 }
- 后端验证时检查 Token 是否过期。
- 在生成 Token 时,添加
(2) 长期有效的 Refresh Token
- 用于延长会话有效期,通常有效期较长(如 7 天或 30 天)。
- 客户端用 Refresh Token 获取新的 Access Token。
- 实现:
- 在 Access Token 过期时,后端验证 Refresh Token 是否有效,签发新的 Access Token。
2. 主动注销机制
在用户登出或敏感操作后,主动让 Token 失效。
实现方式:
后端黑名单机制:
在用户登出时,将 Token 的唯一标识(如
jti
或 Token 哈希值)加入黑名单。在每次请求时,检查 Token 是否在黑名单中。
黑名单存储可以使用 Redis 实现:
javascriptconst redis = require("redis"); const client = redis.createClient(); function addToBlacklist(tokenId) { client.set(tokenId, "blacklisted", "EX", 3600); // 设置过期时间 } function isBlacklisted(tokenId, callback) { client.get(tokenId, callback); }
删除 Refresh Token:
- 在数据库中存储 Refresh Token,当用户注销时,移除对应记录。
3. IP 和设备绑定失效
限制 Token 的使用环境,通过校验客户端信息使 Token 失效。
实现方式:
- 在 Token 中嵌入客户端的 IP、User-Agent 等信息。
- 后端每次校验 Token 时,验证这些信息是否与当前请求一致。
- 示例:json
{ "sub": "1234567890", "name": "John Doe", "ip": "192.168.0.1", "userAgent": "Mozilla/5.0" }
4. 超时失效(会话过期)
Token 在一段时间未使用后自动失效。
实现方式:
设置滑动过期时间(Sliding Expiration):
- 每次使用 Token 时,刷新其过期时间。
- 示例:用户请求刷新时,更新 Token 的过期时间。javascript
function generateToken(payload) { const expiration = Math.floor(Date.now() / 1000) + 900; // 15 分钟 return jwt.sign({ ...payload, exp: expiration }, secretKey); }
服务端记录 Token 的最后使用时间:
- 结合 Redis 或数据库记录最后一次验证时间,判断是否超时。
5. 异常检测失效
当检测到异常行为时,强制让 Token 失效。
触发场景:
- 检测到多地点或多设备登录同一账号。
- 多次失败的登录尝试。
- 短时间内的高频请求。
实现方式:
- 使用异常事件触发 Token 加入黑名单。
- 例如:
- 一个用户从多个地理位置使用同一 Token 时,触发安全机制强制失效。
- 通过 Redis 或数据库存储设备和位置信息。
6. 强制失效机制
管理员手动或系统定时让所有 Token 强制失效。
实现方式:
- 全局签发版本号 (Token Versioning):
- 为每个用户分配一个版本号,在 Token 中携带。
- 当需要强制失效时,更新版本号,旧 Token 即失效。
- 示例:
- 用户数据库记录:json
{ "userId": 123, "tokenVersion": 2 }
- Token Payload:json
{ "userId": 123, "tokenVersion": 2 }
- 用户数据库记录:
总结:
Token 失效机制的核心在于多层防护:
- 时间约束:通过有效期限制使用范围。
- 行为约束:结合用户行为检测异常。
- 动态控制:主动注销、全局失效等增强安全性。
根据业务场景合理组合这些机制,既能提高安全性,也能避免用户体验受损。
二、token 失效原因
Token 失效的原因主要涉及以下四个方面:
- 时间相关:如超时、滑动过期机制缺失。
- 用户行为:如注销、误操作。
- 环境变动:如设备冲突、网络环境变化。
- 安全策略:如异常检测、权限调整。
三、token 失效解决方案
1. 短期 Token + 刷新机制(用户登录返回两个 JWT)
场景:用户登录后需要持续访问资源,短期 Token 过期可能中断操作。
解决方案:
- 短期 Access Token:
- 设置短生命周期的 Access Token(如 5-15 分钟)。
- 通过短期有效 Token 减少暴露时间,提高安全性。
- 长期 Refresh Token:
- 通过 Refresh Token 在后台静默获取新的 Access Token,无需用户重复登录。
- 实现步骤:
- 用户登录后,服务器返回 Access Token 和 Refresh Token。
- Access Token 过期时,客户端自动使用 Refresh Token 请求新的 Access Token。
- 如果 Refresh Token 也失效,要求用户重新登录。
优点:
- 提高安全性,减少长期 Token 暴露风险。
- 静默刷新不会影响用户体验。
注意事项:
- Refresh Token 存储在安全位置(如 HttpOnly Cookie)。
- 设计 Refresh Token 的失效机制,避免滥用。
2. 提前刷新机制
场景:用户操作较长时间未主动发出请求,Token 可能在用户下次操作时过期。
解决方案:
- 客户端定时检测 Token 剩余有效时间,提前触发刷新。
- 刷新条件:
- Token 剩余有效时间低于某个阈值(如 2 分钟)。
- 实现步骤:
- 客户端在获取 Token 后开始倒计时。
- 每次发送请求时,检查 Token 是否即将过期。
- 如果接近过期时间,提前请求新的 Token。
优点:
- 减少因 Token 过期导致的中断操作。
- 提高用户体验,避免频繁登录。
3. 滑动过期机制 (Sliding Expiration)
场景:用户频繁操作,Token 需要不断续期,但又不希望 Refresh Token 被频繁调用。
解决方案:
- 每次使用 Token 时,更新其过期时间。
- 实现方式:
- 后端重新签发带有新过期时间的 Token。
- 或通过 Redis 等缓存技术,更新 Token 的会话有效时间。
- 示例逻辑:
- 用户发送请求时,检查 Token 的剩余有效时间。
- 如果 Token 快要过期,重新签发 Token 并返回。
- 如果请求超时,Token 失效,用户需重新登录。
优点:
- 保持 Token 活跃性,避免频繁重新登录。
- 安全性可控,可配合请求频率限制。
4. 单点登录 (SSO) 与全局注销
场景:用户通过单点登录访问多个系统,Token 在某一系统失效后需要同步更新。
解决方案:
- Token 版本控制:
- 每个用户记录一个全局版本号 (
tokenVersion
)。 - Token 中包含版本号,验证时对比服务器记录。
- 版本更新后,旧版本的 Token 即失效。
- 每个用户记录一个全局版本号 (
- 集中 Token 存储:
- 使用 Redis 或数据库集中存储 Token。
- Token 失效时,立即从存储中删除或标记为无效。
优点:
- 提高统一管理能力,支持多系统协同。
- 实时注销功能提高安全性。
5. 异常处理机制
场景:用户因网络问题或操作不当导致 Token 失效,影响正常使用。
解决方案:
- 友好提示与自动重试:
- 前端检测到 Token 失效(如 401 状态码)后,提示用户重新登录或自动刷新 Token。
- 示例:javascript
axios.interceptors.response.use( (response) => response, async (error) => { if (error.response.status === 401) { // 自动刷新 Token const newToken = await refreshAccessToken(); error.config.headers["Authorization"] = `Bearer ${newToken}`; return axios.request(error.config); } return Promise.reject(error); } );
- 用户通知:
- 如果 Token 长时间未使用,主动向用户发送重新登录通知。
优点:
- 提高用户体验,减少因失效导致的困惑。
6. 备选方案:双 Token 验证
场景:高敏感操作需要额外验证。
解决方案:
- 结合 Access Token 和 CSRF Token:
- 常规操作使用 Access Token。
- 高敏感操作(如转账、密码修改)需额外验证 CSRF Token。
- 强化请求签名机制,确保每次请求的唯一性。
总结:
根据业务需求选择合适的 Token 失效解决方案,推荐组合使用以下策略:
- 短期 Access Token + 长期 Refresh Token(基本方案)。
- 提前刷新机制(提升用户体验)。
- 滑动过期机制(适用于长时间操作)。
- 全局 Token 管理(适用于 SSO 场景)。
四、黑白名单方案
- 黑名单方案:所有黑名单中的 JWT 将不可使用。
- 白名单方案:不在白名单中的 JWT 将不可使用。
1. 黑名单方案
机制:
将被禁止使用的 JWT 标记到黑名单中,无论其是否在有效期内,均拒绝该 Token 的访问请求。
实现方式:
- 存储黑名单:
- 使用 Redis 或数据库存储黑名单中的 JWT 或其唯一标识(如 JTI)。
- 每次请求时校验当前 JWT 是否在黑名单中。
- 加入黑名单的场景:
- 用户主动注销。
- 系统检测到异常行为(如伪造、暴力请求等)。
- Token 被标记为强制失效(如密码修改或权限调整)。
- 定期清理:
- 定时清理黑名单中过期的 JWT,以减少存储压力。
优点:
- 灵活性:允许根据需求动态使特定 JWT 失效。
- 安全性:支持主动失效,防止已被盗用的 Token 被继续使用。
缺点:
- 额外存储开销:需要维护一份黑名单存储。
- 性能消耗:每次请求需要检查 Token 是否在黑名单中。
适用场景:
- 用户可能主动注销,或需要强制下线的场景。
- 支持动态控制和实时调整 Token 有效性的需求。
2. 白名单方案
机制:
只有白名单中的 JWT 才被允许使用,未在白名单的 JWT 即使格式正确、未过期也将被拒绝。
实现方式:
- 存储白名单:
- 将所有有效 JWT 或其唯一标识(如 JTI)存储在 Redis 或数据库中。
- 在每次请求时验证当前 JWT 是否存在于白名单中。
- 白名单的更新:
- 用户登录成功后,将新生成的 JWT 添加到白名单。
- 用户注销、超时或黑名单策略生效时,从白名单中移除相应的 JWT。
- 配合失效机制:
- 设置白名单中 JWT 的过期时间与其签发时的
exp
保持一致。 - 失效后自动从白名单中清理。
- 设置白名单中 JWT 的过期时间与其签发时的
优点:
- 更严格的控制:任何非白名单的 Token 都会被拒绝,即使其未被盗用或未过期。
- 可实现全局注销:随时可以清空白名单,迫使所有用户重新登录。
缺点:
- 额外存储和性能开销:所有有效 Token 必须存储,尤其在高并发场景下存储量较大。
- 依赖后端验证:JWT 的无状态特性被削弱,每次请求都需查询白名单。
适用场景:
- 高敏感操作或严格权限管理。
- 需要更高安全性,要求任何异常 Token 都无法访问资源。
对比分析
特性 | 黑名单方案 | 白名单方案 |
---|---|---|
存储需求 | 仅存储被禁止的 JWT | 存储所有有效 JWT |
性能消耗 | 请求时校验是否在黑名单中 | 请求时校验是否在白名单中 |
灵活性 | 动态标记失效,易于扩展 | 更严格控制,适用于高敏感场景 |
安全性 | 基于已有 JWT 的额外保护 | 所有 JWT 必须经过验证才能使用 |
适用场景 | 用户主动注销或异常检测场景 | 高敏感操作、严格权限管理场景 |
五、其他问题
如何基于 JWT 进行身份验证?
步骤如下:
- 用户向服务器发送用户名、密码、验证码等信息用于登录系统。
- 如果用户用户名、密码、验证码等信息校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
- 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
- 服务端检查 JWT 并从中获取用户相关信息。
两点建议:
- 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
- 请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中(Authorization: Bearer Token)。
如何防止 JWT 被篡改?
对于 JWT 而言,即使 JWT 被泄露或者捕获,黑客也没有办法篡改 TOKEN.因为服务端拿到了 JWT 会解析出其中的 Header、Payload、以及 Signature,通过密钥再次生成一个 Signature 与 JWT 中的 Signature 对比。所以黑客只要不知道我们的密钥是没有办法伪造 Token 的。
如何防止被抓包伪造获取 token?
1. 加密与验证机制
- 使用 HTTPS:确保所有通信内容(包括 Token)经过加密传输。
- Token 签名与验证:采用 JWT 等方式,对 Token 进行签名,并在服务器端验证签名的有效性。
- Token 加密:对 Token 内容加密,确保即使被窃取也无法直接使用。
2. 限制 Token 使用范围
- 短生命周期 Token:设置较短的有效期,并通过 Refresh Token 机制刷新。
- 绑定客户端信息:将 Token 与 IP 地址、User-Agent 等绑定,防止跨设备使用。
- 双 Token 验证:配合 CSRF Token 增强安全性。
3. 防止伪造请求与抓包
- 请求签名机制:加入
timestamp
和nonce
,对请求生成唯一签名,服务端验证合法性。 - 动态密钥与加密:在前后端通信中使用动态生成的密钥进行加密。
- 前端防调试:混淆代码,检测调试工具,防止攻击者直接分析前端逻辑。
- CORS 限制:配置服务器 CORS 策略,验证请求来源。
4. 用户行为与监控
- 登录设备管理:记录 Token 使用的设备和 IP,异常时失效 Token。
- 限速与防暴力破解:限制获取 Token 的接口调用频率,加入 CAPTCHA。
- 实时监控与报警:监控异常 Token 活动,触发安全报警。
六、完整 JWT 示例
JWT 示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpc3MiOiJhdXRoLm15YXBwLmNvbSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJteWFwcCIsImV4cCI6MTcxNjI0OTY5NiwiaWF0IjoxNzE2MjQ2MDk2LCJqdGkiOiJhYmNkMTIzNCIsIm5hbWUiOiJKb2huIERvZSIsImVtYWlsIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4ifQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header:
json{ "alg": "HS256", "typ": "JWT" }
Payload:
json{ "iss": "auth.myapp.com", "sub": "1234567890", "aud": "myapp", "exp": 1716249696, "iat": 1716246096, "jti": "abcd1234", "name": "John Doe", "email": "john.doe@example.com", "role": "admin" }
Signature:
一段由密钥生成的哈希值。