3 分钟用 Go 写完验证码,面试官现场发 Offer

2022-05-10 08:55:47 浏览数 (1)

我记得在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、我们一般都只在业务里面去校验。

最后来看下效果吧:

0 人点赞