必须自定义中间件检查用户must_change_password字段,若为true且请求路径不在白名单(如/auth/change-initial-password、/login等)内,则Abort()拦截;白名单需精确匹配,数据库字段应设于users表并加索引,改密接口须校验身份、密码强度并立即失效旧token。

如何在 Gin 中拦截未改初始密码的用户请求
必须在用户完成密码修改前,阻止其访问除修改密码页以外的所有路由。Gin 本身不提供内置的“强制修改密码”中间件,得自己写逻辑判断并结合 Abort() 控制流程。
核心思路是:每次请求时检查当前登录用户的 must_change_password 字段(或类似标志),若为 true,且当前请求路径不是 /auth/change-initial-password 或登录/登出等白名单路径,则直接重定向或返回 403。
- 推荐用 Gin 的全局中间件(
Use())统一处理,避免漏掉某个路由组 - 判断依据不能只靠 session 或 token 解析结果,必须查数据库确认该用户是否仍处于“需强制修改”状态——因为管理员可能已后台解除限制
- 注意区分 API 和页面跳转:前端是 SPA 时,后端应返回
403+ JSON 提示,而非重定向;如果是服务端渲染(如 HTML 模板),才用c.Redirect()
白名单路由怎么配置才不绕过校验
很多开发者把登录、登出、健康检查等接口加进白名单就完事,但漏了静态资源、CSS/JS 文件、Swagger 文档入口,导致攻击者能绕过校验直接访问 /swagger/index.html 然后调用任意 API。
白名单必须显式声明,不能靠“非敏感路径默认放行”。建议用路径前缀 + 精确匹配组合定义:
- 允许:
/login、/logout、/auth/change-initial-password、/healthz - 禁止:
/api/下所有路径(即使只是/api/v1/users/me)——除非明确加入白名单 - 静态资源若走 Gin 提供(如
StaticFS),也得单独放行/static/,否则会因校验失败返回 403
别用正则模糊匹配白名单,容易误放。例如 ^/api/.* 会把 /api/v1/auth/change-initial-password 也放过去,破坏强制逻辑。
数据库字段设计与查询性能影响
常见错误是把“是否需改初始密码”存在用户表外的独立配置表里,每次请求都跨表 JOIN 查询,拖慢响应。应该直接加字段到 users 表,比如 must_change_password BOOLEAN DEFAULT true。
- 字段名别用
is_first_login这类易歧义的,它无法表达“已登录但未改密”的中间态 - 更新该字段时务必用原子操作:用
UPDATE users SET must_change_password = false WHERE id = ? AND must_change_password = true,防止并发重复提交导致状态错乱 - 如果用户量极大(千万级),考虑给该字段加索引——虽然
BOOLEAN区分度低,但配合WHERE must_change_password = true的高频查询,索引仍有加速作用
修改密码接口如何防重放和越权
强制改密接口(如 POST /auth/change-initial-password)一旦被绕过或滥用,等于开放了任意用户密码重置入口。
- 必须校验当前登录用户身份:从 token/session 取出
user_id,再查数据库确认该用户确实处于must_change_password = true状态 - 禁止接受
user_id作为请求参数传入,否则 A 用户可构造请求帮 B 用户改密 - 新密码要校验强度(如长度、大小写字母、数字),但别在前端做全部校验——后端必须重复验证,防止绕过
- 成功修改后,立刻使旧 token 失效(删 Redis 中的 session,或更新
users.token_version字段并校验版本号)
最常被忽略的是:改密成功后没清空用户其他活跃 session,导致旧密码还能继续登录——这会让整个强制逻辑形同虚设。


















