目录
1、什么是验证码?
简单来说,验证码就是一种用来区分用户是人类还是程序的工具。其核心目的是阻止恶意自动化程序(脚本、爬虫)滥用网络服务,允许合法的用户正常访问。
1.1、验证码有哪些类型?
文本验证码:
最常见的验证码类型,通常包含数字和字母的组合,通过扭曲、变形、添加干扰元素(如点阵、线条)等方式增加识别难度,广泛用于登录、注册、评论等场景。
图形验证码:
用户需要完成特定的图形操作,如拼图、滑动滑块等来进行验证,适用于需要更高安全性的场景,如金融、支付等。
社交账号验证:
用户通过绑定的社交账号(如微信、QQ、Facebook 等)完成验证,无需输入验证码,适用于需要快速验证的场景,如登录、注册等。
其他:
除此之外还有音频验证码(服务于视障人士)、滑动验证码、算数验证码、行为验证码等。
1.2、验证码的用途?
1、防暴力破解:
有些攻击者会使用自动化程序尝试大量的密码组合来破解用户账户,但有了验证码之后脚本无法自动试别验证码。
2、防爬虫:
在网络上游荡着大量的爬虫程序,不断爬取着网站上的各种信息,不仅占用服务器资源,也会威胁到网站的数据安全,通过验证码可以有效防止爬虫爬取。
3、防接口滥用、恶意注册:
通常与接口调用次数限制配合使用,在用户注册或者需要使用资源的页面中,防止恶意的调用。
2、base64captcha验证码库
目前go语言中常用的验证码库有base64captcha和go-captcha,本文采用了base64captcha库,base64captcha 是一个用于 Go 语言的第三方库,主要用于生成和校验验证码。它通过生成 Base64 格式的图片或音频验证码,方便在 Web 应用中集成验证码功能。
3、go语言实现验证码生成、校验
3.1、配置验证码驱动类型
// 配置验证码驱动为字符验证码
var Driverstring = base64Captcha.DriverString{
Height: 60,
Width: 240,
NoiseCount: 1, //噪点数量
ShowLineOptions: 1, //是否显示线条
Length: 4, //长度
Source: "0123456789qazwsxedcrfvtgbyhnujmikolpABCDEFGHJKLMNPQRSTUVWXYZ", //字符源
}
base64captcha库可以生成纯数字类型、纯字母类型、数字+字母类型、数学公式、汉字、音频等多种类型的验证码,不同类型验证码其结构体内部字段不同,以数字+字母类型为例。
3.2、配置验证码存储空间
// 配置验证码存储,可选择内存或者redis,内存可以选择NewMemoryStore或者DefaultMemoryStore
//Store有两个参数,GCLimitNumber(最大记录数,默认为10240),Expiration(过期时间,默认是10分钟)
var Store = base64Captcha.DefaultMemStore
验证码可以使用默认的内存存储或者redis,由于存储在内存中没法对其进行持久化操作,所以在生产环境中优先选择redis存储,本文作为示例,使用内存进行存储。
内存存储:简单易用,低延迟,无网络开销,但无法进行数据持久化,扩展性较差。
redis存储:性能高,支持分布式存储,但会增加网络开销,同时具有一定的复杂度。
3.3、生成验证码
// 生成验证码
func (c *Captcha) GenerateCaptcha(driver base64Captcha.Driver, store base64Captcha.Store) (id, b64s, answer string, err error) {
captcha := base64Captcha.NewCaptcha(driver, store)
return captcha.Generate()
}
在返回值中,id为生成的验证码的id。b64s为base64格式的图片,可传输给接口调用者,让用户来辨认结果。answer为正确答案,正确答案不可传给调用者。
3.4验证校验码
func (c *Captcha) VerifyCaptcha(id, answer string, store base64Captcha.Store) bool {
return store.Verify(id, answer, true)
}
校验码的结果为true或者false。
4、使用gin框架实现生成和解析验证码
4.1、生成校验码
func main() {
var captchaf captcha.Captcha
router := gin.Default()
login := router.Group("login")
{
//生成验证码
login.GET("getcaptcha", func(c *gin.Context) {id, b64, _, err := captchaf.GenerateCaptcha(&captcha.Driverstring, captcha.Store)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"id": id, "b64": b64})
}
})
router.Run(":8080")
}
生成的base64格式校验码通过某些在线工具转换成图片如下所示(生产环境下不要用这些在线工具,避免信息泄露)
4.2、验证校验码
//校验验证码
login.POST("verifycaptcha", func(c *gin.Context) {
//结构体内字段必须要用大写开头,否则无法被正确绑定
type userans struct {
Id string `json:"id"`
Answer string `json:"answer"`
}
var ans userans
err := c.ShouldBindJSON(&ans)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
} else {
isCorrect := captchaf.VerifyCaptcha(ans.Id, ans.Answer, captcha.Store)
c.JSON(http.StatusOK, gin.H{"isCorrect": isCorrect})
}})
5、接口频繁调用问题
5.1、redis方案:
正常的生产环境中,限制接口频繁调用主要是使用redis对接口调用进行限制。由于本文未使用redis,以下代码仅作redis方案的参考演示,主要对通过缓存进行存储的方案进行解释。
var store = base64Captcha.DefaultMemStore
var redisClient = NewRedisClient()type CaptchaHandler struct{}
func (h *CaptchaHandler) GetCaptcha(c *gin.Context) {
ip := c.ClientIP() // 获取客户端 IP 作为唯一标识
key := "captcha_request:" + ip// 从 Redis 获取当前 IP 的访问次数
count, err := redisClient.Get(ctx, key).Int()
if err != nil && err != redis.Nil {
zap.L().Error("Redis 获取失败", zap.Error(err))
c.JSON(500, gin.H{"error": "服务器错误"})
return
}// 判断是否超过访问限制
maxRequests := 5 // 最大访问次数
if count >= maxRequests {
c.JSON(429, gin.H{"error": "请求过于频繁,请稍后再试"})
return
}// 生成验证码
driver := base64Captcha.NewDriverDigit(80, 240, 4, 0.7, 80)
cp := base64Captcha.NewCaptcha(driver, store)
id, b64s, _, err := cp.Generate()
if err != nil {
zap.L().Error("验证码生成失败", zap.Error(err))
c.JSON(500, gin.H{"error": "验证码生成失败"})
return
}// 更新 Redis 中的访问次数
redisClient.Incr(ctx, key)
redisClient.Expire(ctx, key, 60*time.Second) // 设置过期时间为 60 秒c.JSON(200, gin.H{
"captcha_id": id,
"captcha_image": b64s,
})
}
5.2、缓存方案:
在查询相关资料之后,我了解到了一种方案,其过程是统计每一个调用者ip的调用次数,当调用次数大于限定值之后触发接口调用到达上限,我称之为map+gorouting方案,其代码如下:
map+gorouting方案:会创建大量的 goroutine
,方式简单,但不够灵活。如果接口调用非常频繁,可能会对系统性能产生影响。
func generateCaptcha(c *gin.Context) {
ip := c.ClientIP()
mu.Lock()
defer mu.Unlock()count := captchaCountMap[ip]
if count >= maxCaptchaCount {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many captcha requests"})
return
}driver := base64Captcha.NewDriverString(80, 240, 6, 1, 4, "1234567890ABCDEFGHJKLMNPQRSTUVWXYZ", nil, nil, nil)
store := base64Captcha.NewMemoryStore()
captcha := base64Captcha.NewCaptcha(driver, store)
id, b64s, err := captcha.Generate()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate captcha"})
return
}captchaCountMap[ip] = count + 1
go func(ip string) {
time.Sleep(10 * time.Minute)
mu.Lock()
delete(captchaCountMap, ip)
mu.Unlock()
}(ip)c.JSON(http.StatusOK, gin.H{
"id": id,
"captcha": b64s,
})
}
但由于其使用了go routing来进行统计,每有一个调用者ip,都会启用一个新的go routing 这种方法会占用大量的运算资源,感觉不是很合理,为此我写了一种次数+时间戳的方案,其代码如下所示:
次数+时间戳方案:这种方式可以精确控制每个 IP 的调用次数和过期时间。只使用一个 map
和一个互斥锁,资源消耗较小。不需要为每个 IP 创建独立的 goroutine
。
login.GET("generatecaptcha", func(c *gin.Context) {
ip := c.ClientIP()
fmt.Println("ip地址:", ip)
mu.Lock()
defer mu.Unlock()
count := captchamap[ip]// 检查是否过期
currentTime := uint(time.Now().Unix())
if currentTime-count.times >= uint(expiration.Seconds()) {
count.num = 0
count.times = currentTime
}// 当调用次数大于最大次数时
if count.num >= maxCaptchaCount {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "接口调用次数过多"})
return
}// 生成验证码
id, b64, answer, err := captchaf.GenerateCaptcha(&captcha.Driverstring, captcha.Store)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusOK, gin.H{"id": id, "b64": b64, "answer": answer})
}// 更新调用次数和时间戳
count.num++
captchamap[ip] = count
})