我记得在17年那会儿网站登录注册这些随处都可以看到下面这种验证码:
但是好像近些年,这种验证码消失了,出现了特别多的人机验证,如下图:
真的是要感叹技术的发展真的不要太爽了。
人机校验
可能很多同学还不知道他的用处:
他最大的用处就是鉴别是人在操作你的产品还是机器。
比如:最常见的注册、登录业务,如果你不上一些验证手段,可能人家就可以随便找一个工具,直接对你注册接口疯狂输出,让你分分钟拥有十几万神秘用户。
在人机验证没出来之前,人们用的最多的就是用验证码来拦截,要注册或者登录,必须输入验证码里面的内容。
但是随着图片识别技术的发展,这种技术几乎已经失守。
但是一看价格:
以上阿里和腾讯两家的价格,不算特别便宜哈。
前后端分离下实现验证码服务的逻辑
虽然人机校验好处多多,但是介于价格可能很多公司或者个人还是会望而却步。
其实刚上的新服务,前期还是可以先使用验证码来鉴别的,到中后期再接入人机也是可以的。
验证码的逻辑
在传统的单体服务,非前后端分离的情况下,我们可以使用 session 来存储,整个流程可以像下图这样走:
但是现在都前后端分离了,请求会话都是无状态的,该怎么实现呢?
实现方式可能有很多,但是我个人建议可以借鉴下人机交互的逻辑,如下图所示:
这里我们把会话和验证码分离开了,只要需要用到验证码的地方,都可以去请求这个接口,在下一次请求的时候带上返回的 key 和输入的值就可以了。
这个流程其实还是有很多漏洞在里面,实际上生产肯定不能直接这么简单的上,还要加上很多其他技术在里面,比如把生成的验证码和下一步请求的地址关联起来、签名呀这些。
但是这已经能拦截一大批攻击者了。
这种做法和人机验证最大的区别在于,我们生产的验证码容易别人用工具识别出来,人机验证的他们有一套算法去防止被机器识别出来。
后期如果要换成人机也非常容易,因为流程是一样的。
基于 Gin 实现一套验证码
上面说了那么多的理论逻辑,下面开始上代码:
完整的代码可以到我们官方的 Github 库查看:
代码语言:javascript复制https://github.com/GoLangStackDev/captcha-demo.git
使用到的库
这里我们处理 Gin 之外还要用到 captcha
库:
官方 GitHub 地址:github.com/dchest/captcha
这个库功能非常强大,他支持生成图片验证码和音频验证码:
实现思路一样的,代码几乎一样,只是类型不一样,我们主要以生成图片为准。
安装库
我们需要安装两个库:
代码语言:javascript复制go get github.com/dchest/captcha
go get github.com/gin-gonic/gin
先实现工具类
这个工具类我们专门用来处理验证码:
代码语言:javascript复制// Captcha 方便后期扩展
type Captcha struct {}
// 单例
var captchaInstance *Captcha
func Instance() *Captcha {
if captchaInstance==nil {
captchaInstance = &Captcha{}
}
return captchaInstance
}
我们声明了一个结构体,方便后期在 captcha 这个库上进行扩展。
代码语言:javascript复制// CreateImage 创建图片验证码
func (this *Captcha) CreateImage() string {
length := captcha.DefaultLen
captchaId := captcha.NewLen(length)
return captchaId
}
创建验证码也很容易,我们这里直接全部使用他默认的配置,生产6位数的数字验证码,后期有需要可以参考 captcha 库进行调整配置。
这里会返回一个 ID 给我们,这个 ID 就是刚我画的流程图里面的 key,他关联了一个随机数,也就是图片的数字。
这里他存放在哪里的呢?
默认是内存,所以重启程序后就可能找不到已经生成的验证码了,但你可以修改他存放在哪里。
代码语言:javascript复制// Reload 重载
func (this *Captcha) Reload(captchaId string) bool {
return captcha.Reload(captchaId)
}
因为不可能用户每次都能输对,所以有些时候用户不能识别的情况下就需要进行重新生成随机数,也就是重新生成一张图片,但是 key 也就是 ID 是不能变的,此时就要用到重载。
代码语言:javascript复制// Verify 验证
func (this *Captcha) Verify(captchaId,val string) bool {
return captcha.VerifyString(captchaId, val)
}
这就是验证了,传入 ID 和 用户输入的值就可验证了。
代码语言:javascript复制// GetImageByte 获取图片二进制流
func (this *Captcha) GetImageByte(captchaId string) []byte {
var content bytes.Buffer
err := captcha.WriteImage(&content, captchaId, captcha.StdWidth, captcha.StdHeight)
if err!=nil {
log.Println(err)
return nil
}
return content.Bytes()
}
最后就是关键了,怎么把图片输出给用户,captcha 库他会生成一个图片的二进制流,你只需要把这个二进制流返回回去即可得到图片。
Gin部分的代码
这里都只展示关键部分的代码:
代码语言:javascript复制// 创建
// 这里方便看到效果 我用的 GET 请求,实际生产最好不要用 GET
r.Handle("GET", "/captcha/create", func(c *gin.Context) {
imgId := captcha.Instance().CreateImage()
c.JSON(http.StatusOK,
gin.H{
"code": 200,
"key": imgId,
"url": "/captcha/img/" imgId,
})
})
首先是创建的接口,这里直接调用我们工具类的 CreateImage 方法拿到 key 即可。
这里的 URL 和下面这个现实的 API 关联。
代码语言:javascript复制// 现实图片
r.Handle("GET", "/captcha/img/:key", func(c *gin.Context) {
captchaId := c.Param("key")
c.Writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
c.Writer.Header().Set("Pragma", "no-cache")
c.Writer.Header().Set("Expires", "0")
c.Writer.Header().Set("Content-Type", "image/png")
// 重载一次
captcha.Instance().Reload(captchaId)
// 输出图片
c.Writer.Write(captcha.Instance().GetImageByte(captchaId))
})
我们每请求一次这个 key 就重载刷新一下他的 Code,方便前端刷新。
前端只需要在这个地址后面加上随机参数即可实现刷新验证码。
最关键的地方就是要设置客户端的请求头里面不能让他缓存。
代码语言:javascript复制// 校验
r.Handle("GET", "/captcha/verify/:key/:val", func(c *gin.Context) {
captchaId := c.Param("key")
val := c.Param("val")
if captcha.Instance().Verify(captchaId,val) {
c.JSON(http.StatusOK, gin.H{"code": 200})
}else{
c.JSON(http.StatusOK, gin.H{"code": 400})
}
})
最后就是校验了,正常来说这个接口是不能放出来了的,因为:
1、 captcha 库,只要校验一次,不管成功失败他的 ID 就失效了。
2、我们一般都只在业务里面去校验。
最后来看下效果吧: