验证码必须绑定会话或请求指纹,不能仅靠前端传来的captchaId校验;安全做法是生成时存入session或用IP+UA哈希作key前缀,生产环境必须使用Redis存储并设置2分钟过期,校验成功后立即删除ID,且禁止复用。

验证码必须绑定会话或请求指纹,不能只靠前端传来的 captchaId
常见错误是前端拿到 captchaId 后直接 POST 到校验接口,后端仅调用 captcha.VerifyString(id, input)。这会导致攻击者遍历 ID(比如从 1 到 1000)批量重放请求,绕过所有防刷逻辑。
安全做法是把验证码和具体上下文强绑定:
- 有 session 场景(如登录页):生成时存
session.Set("captcha_id", id),校验前先比对session.Get("captcha_id") == id - 无 session 场景(如纯 API):用
hash.Sum256([]byte(c.ClientIP() + c.GetHeader("User-Agent"))).String()作 key 前缀,再拼上随机 salt,避免 IP 池穿透 - 绝对不要在 URL query 或 body 中裸传
captchaId作为唯一校验依据
必须替换默认内存存储,否则重启即失效且无法跨实例共享
base64Captcha 默认用 store := captcha.NewMemoryStore(),所有验证码存在进程内存里。服务重启后全部丢失;多实例部署时,A 实例生成的 ID 在 B 实例查不到,校验必失败。
正确做法是实现 captcha.Store 接口并注入:
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 开发/测试环境可用
github.com/gofrs/flock加文件锁模拟共享存储(不推荐长期用) - 生产必须用 Redis:自定义
RedisStore,Set时带EX 120(2 分钟有效期),Get和Verify必须设clear=true,确保校验成功后立即删除 - 初始化时务必调用
captcha.SetCustomStore(yourRedisStore),否则所有后续调用仍走内存
校验成功后必须立即 captcha.Delete(id),且禁止复用 ID
即使用了 Redis 存储,如果校验通过后不删,攻击者可截获一次成功响应,反复重放同一组 id+code,实现无限次通过。
注意几个关键点:
-
captcha.VerifyString(id, input, true)的第三个参数为true才会自动清理,但依赖底层Store.Verify实现是否真正执行了 delete —— Redis 实现里必须显式调用DEL - 更稳妥的做法是手动调用
captcha.Delete(id),哪怕Verify已设clear=true,双重保险 - 生成新验证码前,先用
captcha.Delete(oldId)清掉旧 ID,防止用户连点“换一张”积累多个有效 ID
Gin 路由设计要避开中间件生命周期陷阱
有人把验证码生成写成 Gin 中间件,结果要么每次请求都触发生成(浪费资源),要么校验时中间件已退出、c.Request.Context() 丢失、session 不可用。
根本原因是中间件执行时机与业务逻辑脱节。正确姿势是拆成两个明确路由:
-
GET /captcha:独立 handler,不走鉴权中间件,返回{"id": "xxx", "image": "data:image/png;base64,..."} -
POST /login:在业务 handler 开头手动校验,例如:if !store.Verify(c.Query("captcha_id"), c.PostForm("code"), true) { ... } - 切忌在中间件里调用
c.Next()后再取c.Query或c.PostForm—— 此时 request body 可能已被读空

















