<p>拼图验证码的核心难点在于前端切图与后端校验的一致性,需统一基于原始图尺寸(如320×160)计算缺口坐标cutX,前端传dragX,后端校验abs(dragX - cutX)≤阈值,并结合Redis时效性、IP一致性(取X-Real-IP)完成验证。</p>

拼图验证码的核心难点在前端切图与后端校验一致性
拼图验证码不是单纯生成图片,而是要保证前端展示的“缺口位置”和后端验证时的“用户拖动偏移量”能严格对齐。Gin 本身不处理图像,得靠 image 标准库 + png.Encode 拼合底图与滑块图,关键在于:所有坐标必须基于同一原始图尺寸计算,且不能依赖前端 JS 自行测量(易被篡改或受缩放影响)。建议统一用 320×160 原图尺寸,后端生成时直接记录缺口左上角横坐标 cutX(比如 86),前端只传回用户拖动距离 dragX,后端比对 abs(dragX - cutX) 即可——容错 5px 是经验值,太严用户易失败,太松易绕过。
用 image/draw 裁剪滑块图并合成带缺口的背景图
别用第三方图像库,Go 标准库足够。流程是:先读取一张原始图(original.png),随机选一个横坐标 cutX(范围控制在 [40, 260] 避免贴边),然后裁出宽 60px、高 160px 的滑块区域(含缺口右侧那块),再把该区域从原图中“挖掉”生成背景图。注意两点:draw.Draw 的目标图必须是新创建的 *image.RGBA,且裁剪时用 image.Rect(cutX, 0, cutX+60, 160) 确保矩形精准;合成背景图时,要把挖掉区域用纯色(如 color.RGBA{240,240,240,255})覆盖,否则透明像素会导致前端显示异常。
bg := image.NewRGBA(image.Rect(0, 0, 320, 160))
draw.Draw(bg, bg.Bounds(), src, image.Point{}, draw.Src)
// 挖缺口:用灰色覆盖 [cutX, 0, cutX+60, 160]
draw.Draw(bg, image.Rect(cutX, 0, cutX+60, 160), &image.Uniform{color.RGBA{240,240,240,255}}, image.Point{}, draw.Src)Gin 路由返回 base64 图片 + cutX 并设置短期缓存
不要返回二进制 PNG 流,前端处理 base64 更简单;但 cutX 不能藏在图片里(OCR 可提取),必须单独返回。结构体定义为:type CaptchaResp struct { Image string `json:"image"` CutX int `json:"cut_x"` }。重点是缓存控制:用 c.Header("Cache-Control", "no-store") 禁用浏览器缓存,同时生成时把 cutX 和当前时间戳一起写入 Redis(key 为 sessionID + timestamp),过期设为 120 秒——防止重放攻击。若用户提交时 Redis 中无对应 key 或时间超 120 秒,直接拒绝。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 前端请求
/captcha后,把Image字段赋给<img src="data:image/png;base64,..."> -
CutX存入 JS 变量,用户拖动结束后传给/verify接口 - 务必校验请求头中的
X-Forwarded-For或RemoteIP是否与生成时一致(防 token 劫持)
验证接口必须校验时间、IP、拖动误差三重条件
只比对 dragX == cutX 是危险的。真实场景要叠加:① Redis 中是否存在该 session 对应的 captcha:xxx key;② key 的 TTL 是否 > 0(用 Redis.TTL() 检查);③ 请求 IP 是否与生成时存入的 IP 完全相同(注意 Nginx 转发后需取 X-Real-IP);④ abs(dragX - cutX) 。任意一项失败都返回 <code>400 Bad Request 并清空该 key。示例错误响应:{"ok":false,"msg":"captcha expired or invalid"},不要暴露具体失败原因。
立即学习“go语言免费学习笔记(深入)”;
容易忽略的是并发写入冲突:多个请求同时刷新同一 session 的 captcha,导致旧 cutX 被覆盖。解决方案是在 Redis 写入时加 SET captcha:xxx 86 NX EX 120(NX 表示仅当 key 不存在时才设值),失败则重试生成新图——这比乐观锁更轻量。

















