不能只靠前端校验,因攻击者可绕过浏览器直接发请求;必须在gin后端分层防护:输入解析→身份确认→权限校验→数据清洗→存储加密,且密保答案须bcrypt哈希、接口需二次验证与防重放。

为什么不能只靠前端校验改密保
用户提交新密保问题和答案时,前端弹窗提示“不能为空”或“长度不小于4”,这类校验毫无安全意义。攻击者绕过浏览器直接发 curl 请求,照样能传空字符串、超长恶意 payload 或 SQL 片段。真正的防护必须落在 Gin 的请求处理链上,且要分层:输入解析 → 身份确认 → 权限校验 → 数据清洗 → 存储加密。
AuthMiddleware + 用户上下文绑定是前提
没通过 AuthMiddleware 就进不到修改逻辑,这是第一道门。但注意:该中间件默认只把 user_id 写入 c 上下文,不附带任何角色、状态或登录设备信息。如果你的密保修改要求“必须是最近 24 小时内用密码登录过的用户”,就得在中间件里额外写入时间戳,并在 handler 中检查:
-
c.Set("login_time", time.Now())要在鉴权成功后立即调用 - 修改密保 handler 里用
c.Get("login_time")拿出并判断是否超时 - 别依赖 session 或 cookie 做这个判断——它们可能被复用或劫持
密保答案必须 bcrypt 加盐哈希,不能明文存库
密保答案不是密码,但一样敏感。用户常把密保答案设成生日、手机号、宠物名,这些极易被社工库撞库。一旦数据库泄露,明文密保 = 直接拿到重置入口。
正确做法是用 golang.org/x/crypto/bcrypt 处理:
hashed, err := bcrypt.GenerateFromPassword([]byte(secretAnswer), bcrypt.DefaultCost)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "hash failed"})
return
}
// 存 hashed,不是原答案
- 别用 MD5/SHA 等快速哈希——暴力破解几秒就出结果
- 别自己拼 salt 字符串——
bcrypt内置 salt 生成和嵌入 - 别把 cost 设成 4——默认
bcrypt.DefaultCost(目前是 12)已平衡安全与性能
修改接口必须二次验证,且防重放
仅凭 token 访问 /api/user/security 是危险的。攻击者若截获一个有效 token,就能反复调用该接口覆盖密保。必须加第二道锁:
- 在请求体中强制携带一次性验证码(如短信/邮箱验证码),服务端用
redis.SetEX存 5 分钟,验证后Del - 或要求用户再次输入当前账户密码(注意:要用
bcrypt.CompareHashAndPassword校验,不能查库比对明文) - 所有修改请求必须带
X-Request-ID和签名头(如HMAC-SHA256(body+secret)),防止中间人重放
最易被忽略的是:验证码校验和密码校验都必须用 subtle.ConstantTimeCompare,否则会暴露 timing attack 侧信道——攻击者靠响应时间差就能猜出验证码前几位。











