JWT本身仅提供签名验证和过期时间(exp)控制,无法识别请求是否首次使用,故无法防范有效期内的重放攻击;必须结合服务端状态记录(如Redis存储jti)实现“一次有效、用完失效”。
为什么单纯用 JWT 无法防重放攻击
jwt 自身只是个签名凭证,不包含时效性控制以外的防重放机制。攻击者截获一个合法 authorization: bearer xxx 请求后,在有效期内反复重放,服务端只要签名和过期时间校验通过,就会放行——这跟有没有用户身份无关。
关键点在于:服务端必须能识别“这个 token 是不是第一次被使用”。常见误区是以为加了 exp 字段就够了,其实它只防“过期后重放”,不防“有效期内重放”。
实操建议:
- 必须配合服务端状态记录(如 Redis 存已用过的
jti+ 过期时间),且 TTL 要略大于 token 最大可能网络延迟 + 处理耗时(比如设为 5 分钟) - 不要把
jti设为纯随机 UUID——它得可预测生命周期,否则无法清理;推荐用base64(urlSafe)+timestamp拼接后哈希,或直接用time.Now().UnixMilli()作为 jti 前缀 - 如果用 Redis,避免单 key 大量写入瓶颈:按小时分片,例如 key 为
used_jti:2024052015,value 存 set 或 bitmap
Go 中如何安全生成和验证带防重放能力的 Token
标准 github.com/golang-jwt/jwt/v5 库支持自定义 Claims,但防重放逻辑必须在签发和验证两端显式处理,库本身不介入。
示例关键步骤:
- 签发时:生成唯一
jti(如fmt.Sprintf("%d-%s", time.Now().UnixMilli(), randStr(8))),写入map[string]interface{}后传给jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - 验证时:先调
token.Valid基础校验,再从token.Claims.(jwt.MapClaims)取出jti,查 Redis 是否存在;若存在则拒绝,否则立即SET jti_key "" EX 300 - 注意:
ParseWithClaims的 error 处理要区分jwt.ValidationErrorExpired和自定义的 “jti 已使用” 错误,前者返回 401,后者应返回 403(防止暴露校验顺序)
如何避免时间漂移导致的防重放失效
客户端和服务端系统时间不一致时,iat / nbf 校验可能失败,更危险的是:若依赖 iat 截断旧 token,而客户端时间快 2 分钟,服务端就可能误判“这个刚发的 token 是重放的旧包”。
实操建议:
- 禁止用
nbf或iat做防重放依据;所有时效判断统一走 Redis TTL - 客户端 SDK 必须强制校验服务端返回的
Dateheader,并动态调整本地 token 签发逻辑中的时间偏移(哪怕只用于日志和 debug) - 在 Gin/Middleware 中加入时间差告警:当请求携带的
iat与服务端时间相差 >90s,记录日志但不拦截,方便后续排查设备问题
Redis 故障时的降级策略怎么写才不崩
不能让鉴权模块因 Redis 不可用而整体不可用——这等于主动开后门。但也不能简单跳过防重放校验。
合理做法是分级响应:
- Redis 连接超时或 timeout 错误(如
redis: connection refused、context deadline exceeded):记录错误日志,继续执行后续鉴权(即仅跳过 jti 检查),但给响应头加X-Auth-Downgrade: jti-check-skipped - Redis 返回非空结果(如
redis: nil以外的 error):视为严重异常,返回 500 并中断请求 - 上线前必须压测 Redis 故障场景,确认降级后 QPS 不跌超过 15%,且无 goroutine 泄漏(尤其检查是否忘了
defer cancel())
真正麻烦的不是代码怎么写,而是运营侧没同步更新监控告警——Redis 延迟突增 300ms 时,鉴权延迟会直接翻倍,但如果你只看成功率,可能一周都发现不了。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











