实践分享:在 Go 项目中实现一个安全的密码重置流程
实践分享:在 Go 项目中实现一个安全的密码重置流程
1. 业务场景
在我们的 health-master 项目中,我们需要一个安全且用户体验良好的密码重置功能。用户流程如下:
- 输入邮箱申请重置 收到邮件链接。
- 点击链接 页面显示脱敏后的账号(确认没点错)。
- 输入并提交新密码 链接失效 修改成功。
2. 技术难点与设计方案
2.1 数据库建模
我们不只是存一个 Token 字符串,而是将“重置请求”看作一个有生命周期的资源。
1 | CREATE TABLE tokens ( |
2.2 Token 的安全性:Hash 存储
直接在数据库存储明文 Token 是危险的。我们效仿密码存储的逻辑,在数据库中存储 Token 的 SHA-256 哈希值。
- 生成:
crypto/rand生成 32 字节随机数 发送给用户。 - 存储:
sha256(raw_token)存入token_hash字段。 - 校验: 接收用户传来的 Token 哈希后去数据库匹配。
2.3 原子核销:防止并发重置
为了保证“阅后即焚”(重置一次即失效),我们使用了 PostgreSQL 的原子更新操作。在 ConsumeToken 时,我们通过一行 SQL 同时完成校验与核销:
1 | UPDATE tokens |
2.4 隐私保护:SQL 层面的脱敏
用户打开页面时,我们希望展示其 Email 但不暴露完整地址。为了性能,我们直接在 SQL 中处理:
1 | SELECT |
3. 代码架构实现 (Service 层)
在 ResetPassword 的 Service 实现中,我们利用了 Go 的 命名返回值 (Named Return Parameters) 来优雅地管理事务。这种写法确保了只要有任何一步报错,tx.Rollback() 就会被正确执行。
1 | func (userService *UserService) ResetPassword(ctx context.Context, email, rawToken, newPassword string) (user *model.User, err error) { |
4. RESTful API 设计
为了符合现代 API 规范,我们将重置流程定义为对 password-resets 资源的操作:
POST /password-resets: 创建重置请求。GET /password-resets/:token: 预检查并获取用户信息。PUT /password-resets/:token: 执行更新。
5. TODO:后续优化
这个 PR 已经打下了坚实的基础,后续我们还可以:
- 自动清理: 增加 Cron Job 每天定时清理过期超过 24 小时的 Token 记录。
- 安全审计: 在重置成功后,发送一封“密码已修改”的提醒邮件。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Damingerdai's Blog!
