密码重置 token 必须带签名且限时:用邮箱+时间戳 hmac-sha256 签名并 base64 url-safe 编码,有效期 15–30 分钟;仅接受 post 请求体传 token,禁止 get 参数;验证后立即原子性失效并清空用户所有 session。

密码重置 Token 必须带签名且限时
直接用随机字符串(比如 uuid.NewString())生成 reset token 是危险的——攻击者可能暴力猜测或重放。Gin 本身不提供 token 签名能力,得自己用 crypto/hmac 或 golang.org/x/crypto/nacl/secretbox 加密签名。
推荐做法:用用户邮箱 + 过期时间戳拼接,再用服务端密钥 HMAC-SHA256 签名,最后 base64 URL-safe 编码。例如:base64url.Encode(hmac.Sum256([]byte(email + "|" + expiry.UnixNano())).Sum(nil))。token 有效期严格控制在 15–30 分钟,过期后 time.Now().After(expiry) 必须校验失败。
- 密钥必须从环境变量读取,禁止硬编码在代码里
- 签名原文里必须含时间戳,且验证时只接受未来 5 秒内的偏差(防重放)
- 不要把用户 ID 直接放进 token——容易被反推,用邮箱(已脱敏校验过)更安全
验证路由必须用 POST 且禁用 GET 参数传 token
很多人把 reset token 放在 URL 查询参数里,比如 /reset?token=xxx,这会导致 token 泄露到访问日志、代理缓存、浏览器历史甚至 Referer。Gin 的 c.Query("token") 在这种场景下就是安全隐患源头。
正确方式是只接受 POST /api/v1/password/reset/verify,body 中用 JSON 提交:{"token": "xxx"},然后用 c.ShouldBindJSON(&req) 解析。同时在路由注册时显式禁用 GET:
router.POST("/password/reset/verify", verifyResetTokenHandler)
// 别写 router.GET 或 router.Any
- Gin 默认不拦截重复路径的其他方法,漏掉限制就等于开了后门
- 如果前端非要“点击链接跳转”,链接只能指向静态页面(如 /reset.html),由该页面发 POST 请求,不能带 token 参数
- Nginx 层建议加
log_format过滤掉包含token=的请求日志行
验证通过后必须立即失效旧 token 并清空关联 session
一个 reset token 理论上只能用一次。Gin 没有内置 token 吊销机制,得靠数据库字段或 Redis 记录状态。常见错误是只查 token 是否有效,却没更新它的 used_at 字段或删掉 Redis key。
操作顺序必须是原子的:先用 UPDATE ... WHERE token = ? AND used_at IS NULL AND expires_at > NOW() 尝试更新,检查 RowsAffected == 1;失败则直接返回 400。成功后再查用户、生成新密码 hash、清空该用户所有活跃 session(比如删掉 Redis 里 session:uid:* 的 key)。
- 别用 SELECT + UPDATE 两步——并发请求可能导致重复使用
- 如果用 Redis 存 token,
DEL操作必须紧跟GET之后,且用 Lua 脚本保证原子性 - session 清理不能只删当前设备的 session——重置密码意味着所有终端都应登出
密码更新前必须再次校验当前用户凭证(可选但强烈建议)
有些系统在 /reset/verify 成功后,直接跳到 /reset/set 页面并允许无条件改密。这是漏洞:如果攻击者截获了验证后的跳转,就能绕过二次确认。
更稳妥的做法是在最终提交新密码时,要求携带短期有效的 second-factor token(比如从 /reset/verify 返回的 session_id),或者让前端在提交前调用 /api/v1/user/me 获取当前登录态,并把响应里的 user_id 和 session_sig 一起提交过来,后端比对是否匹配本次重置流程的原始用户。
- 这个 second-factor token 有效期应 ≤ 2 分钟,且绑定 IP 和 User-Agent(做简单指纹)
- 不要依赖 Cookie 自动携带——重置流程中用户很可能已登出,得显式传 header 或 body
- 如果业务允许,优先用邮箱一次性验证码(不是 reset token)作为第二步,比 session 更可控
最麻烦的地方不在 Gin 写法,而在 token 生命周期管理——签发、传输、校验、吊销、清理,每一步都有状态要同步,少一环就可能让重置逻辑形同虚设。











