Refresh Token必须设过期时间并每次刷新轮换,否则攻击者可无限续期;须存哈希值、绑定user_id、expires_at和used字段,校验时三者缺一不可,且需同步Redis黑名单防重放与即时作废。

Refresh Token 为什么不能只存数据库不加过期时间
直接把 refresh_token 存进数据库但不设 expires_at 字段,等于留后门——攻击者一旦拿到它,就能无限续期 access_token,彻底绕过登录验证。必须为每个 refresh_token 绑定明确的过期时间(比如 7 天),且每次成功刷新后要立即作废旧 token、生成新 token 并写入新过期时间。
实操建议:
- 数据库表里至少包含:
token_hash(SHA-256 加盐哈希值,不存明文)、user_id、expires_at、used(布尔值,用于防重放) - 生成
refresh_token时用crypto/rand.Read()生成 32 字节随机字节,再 base64url 编码,避免可预测性 - 校验时先查
token_hash == hash(输入token)且expires_at > NOW()且used == false,三者缺一不可
刷新接口必须校验 access_token 的有效性再换新 token
有些实现跳过 access_token 校验,仅凭 refresh_token 就发新对,这会导致:用户已登出或密码已改,但旧 refresh_token 仍能续命。正确流程是「双校验」——既要验证 refresh_token 有效,也要确认当前 access_token 未被主动注销(例如存在 blacklist 表或 Redis 中的短时效标记)。
实操建议:
- 在签发
access_token时,同时往 Redis 写一条blacklist:uid:{user_id}:{jti},TTL 设为该 token 剩余有效期(如 15 分钟),用于登出和强制失效 -
/auth/refresh接口收到请求后,先解析并验证原始access_token的签名、过期时间、jti是否在黑名单中;全部通过才继续处理refresh_token - 若原始
access_token已失效(过期或被拉黑),直接返回401 Unauthorized,不尝试续期
注销时怎么确保 refresh_token 立即失效
用户点击「退出登录」时,只删掉前端的 access_token 和 refresh_token 是不够的——后端数据库里那条记录还在,下次拿它照样能换新 token。必须同步作废后端存储的 refresh_token 记录。
实操建议:
- 注销接口执行两件事:将对应
refresh_token的used字段置为true(或直接 DELETE),同时往 Redis 写revoked_rt:{hash}带 TTL(比如 24 小时),防止并发注销漏判 - 刷新接口在查库前,先查 Redis 中是否存在
revoked_rt:{hash},存在则拒绝刷新,避免刚注销就立刻重放 - 不要依赖定时任务清理过期
refresh_token,而应在每次刷新前做实时校验 + 清理逻辑(例如:查出所有expires_at 的记录并批量标记为已失效)
JWT 的 jti 字段不是可选项,而是注销关键索引
很多 Go 项目用 github.com/golang-jwt/jwt/v5 生成 token 却忽略 jti(JWT ID),导致无法精准标记某个特定 access_token 为已注销。没有 jti,你就只能靠用户 ID + 时间窗口拉黑,误伤率高、粒度粗。
实操建议:
- 签发
access_token时必须填Claims["jti"] = uuid.NewString(),确保每个 token 全局唯一 - 把
jti当作黑名单主键存进 Redis:SET blacklist:jti:{jti} 1 EX 900(TTL 对齐 token 过期时间) - 解析 token 后先查
blacklist:jti:{jti},命中则直接拒绝,不用等后续校验走完才发现已被注销
Go 的 HTTP handler 里没魔法,续期逻辑就是几行条件判断+数据库操作+JWT 重签;真正容易翻车的是状态一致性——token 生效、作废、过期、拉黑这些状态在多个存储(DB、Redis、客户端)之间不同步。别想着“先跑起来再补”,从第一版就要定义清楚每种状态变更的原子操作边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











