gin-contrib/sessions与验证码不搭,因验证码需独立存取且常在无session场景(如未登录页)使用;应改用redis或map自定义存储,配合base64Captcha生成图片并返回ID,校验时通过captcha_id查值比对。

为什么 gin-contrib/sessions 和验证码不搭?
生成图片验证码时,你真正需要的是把随机字符串存到服务端、把图片二进制吐给前端,而不是靠 session 自动管理——gin-contrib/sessions 默认绑定 cookie + 内存/redis,但验证码校验前常要绕过 session 中间件(比如登录页还没登录,哪来的 session?),强行套用反而导致 session.Get 返回空或 panic。
更直接的做法是:自己用 map[string]string 或 redis 存验证码值,用 UUID 或时间戳+随机数生成 key,key 通过响应头或 JSON 返回给前端,校验时比对即可。
- 避免在中间件里初始化 session.Store,除非你明确需要全站 session 且验证码也走同一套流程
- 内存存储仅适合开发调试;生产必须用
redis,否则多实例下校验必失败 - key 命名建议带前缀如
"captcha:" + uuid,方便 redis 过期清理
用 github.com/mojocn/base64Captcha 生成 PNG 图片流
这个库轻量、无 CGO、支持数字+字母+算术题,且能直接输出 []byte,和 Gin 的 c.Data 天然契合。别用它自带的 HTTP handler,Gin 里要自己调 driver.GenerateIdQuestionAnswer。
关键点是:生成后立刻存入缓存,并设置短过期(如 5 分钟),同时把 ID 返回给前端用于后续校验。
// 示例:生成并返回图片
func GetCaptcha(c *gin.Context) {
id, b64s, err := base64Captcha.GenerateCaptcha("", 4, base64Captcha.ConfigNumber{
Height: 40,
Width: 120,
Noise: base64Captcha.OCR,
})
if err != nil {
c.JSON(500, gin.H{"error": "gen captcha failed"})
return
}
// 存 redis: captcha:id → answer (小写)
rdb.Set(c, "captcha:"+id, strings.ToLower(b64s), 5*time.Minute)
c.Header("Content-Type", "image/png")
c.Data(200, "image/png", driver.ImageToBytes(driver.NewDriverDigit(40, 120, 0, 0, "Arial", 20)))
}
-
base64Captcha.GenerateCaptcha第一个参数是自定义 driver,传空字符串用默认数字驱动 - 别漏掉
strings.ToLower—— 前端输入通常不区分大小写,后端存小写,校验时统一转小写比对 -
driver.ImageToBytes需要重新 new 一个 driver,不能复用GenerateCaptcha里内部创建的(它不暴露)
前端怎么传验证码 ID 和答案?
常见错误是只传图片里看到的字符,却没传对应的 captcha_id,导致后端找不到缓存中的答案。必须前后端约定两个字段:captcha_id 和 captcha_value。
Gin 接收时用 c.PostForm(表单)或 c.ShouldBindJSON(JSON),别用 c.Param 或 c.Query —— 验证码答案不是路径或查询参数。
- 前端示例(fetch):
fetch("/login", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ username: "a", password: "b", captcha_id: "xxx-uuid", captcha_value: "abcd" }) }) - 后端校验逻辑必须先查
rdb.Get(c, "captcha:"+id).Val(),再比对,查不到或为空就直接拒绝 - 校验成功后立即
rdb.Del(c, "captcha:"+id),防止重放
为什么校验总提示“验证码错误”?
90% 是大小写或空格问题。base64Captcha 默认生成的字符串含大小写字母,而用户手输容易忽略大小写,前端又没做 trim 和 toLowerCase,后端也没统一处理。
另一个隐蔽坑是:redis 的 key 过期时间设太短(如 60 秒),用户打开页面慢一点,图片还没看清就过期了;或者设太长(如 24 小时),被恶意刷号攒一堆待校验数据。
- 前后端都强制
.trim().toLowerCase(),后端校验前再做一次strings.TrimSpace(strings.ToLower(input)) - redis TTL 建议 3–5 分钟,够用户操作,也不留垃圾
- 生成接口加简单限流(如
golang.org/x/time/rate),防暴力刷图 - 如果用 Nginx 反向代理,确认没缓存
/captcha路由(加Cache-Control: no-store)
验证码不是越复杂越好,而是要在识别率、安全性、用户体验之间找平衡点。算术题对机器更难,但手机用户输入 “3 + 7 = ?” 比输 “Xk9m” 更快——选哪种,得看你的用户是谁。


















