[网络安全]在go语言中使用base64captcha库实现生成验证码

目录

1、什么是验证码?

1.1、验证码有哪些类型?

1.2、验证码的用途?

2、base64captcha验证码库

3、go语言实现验证码生成、校验

3.1、配置验证码驱动类型

3.2、配置验证码存储空间

3.3、生成验证码

3.4验证校验码

4、使用gin框架实现生成和解析验证码

4.1、生成校验码

4.2、验证校验码

5、接口频繁调用问题

5.1、redis方案:

5.2、缓存方案:


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
})

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值