0


golang gin base64Captcha生成验证码后通过redis存储 以及安全验证思路

在学习过程中,我遇到了一个场景:用户使用邮箱进行登录注册,此时需要向对应邮箱发送验证码。之后用户输入验证码后,我们需要对验证码进行校验。这里我选择使用第三方包base64Captcha生成验证码,并且使用redis作为验证码池进行存储,代码如下:

1.创建一个redis验证码池,确保你已经配置了redis环境,这里必须实现以下base64Captcha中的Store接口

type Store interface {
    // Set sets the digits for the captcha id.
    //验证码存入验证码池的方法,id为键,验证码为值
    Set(id string, value string) error

    // Get returns stored digits for the captcha id. Clear indicates
    // whether the captcha must be deleted from the store.
    //获取验证码的方法
    Get(id string, clear bool) string

    //Verify captcha's answer directly
    //校验验证码的方法
    Verify(id, answer string, clear bool) bool
}

对这个接口的实现方法如下


//自定义一个验证码池
//以redis作为验证码池

var ctx = context.Background()

const CAPTCHA = "captcha:"

type RedisStore struct {
}

// Set sets the digits for the captcha id.
func (r RedisStore) Set(id string, value string) error {
    key := CAPTCHA + id
    err := global.RedisDb.Set(ctx, key, value, time.Minute*2).Err()
    if err != nil {
        log.Println(err.Error())
    }
    return err
}

// Get returns stored digits for the captcha id. Clear indicates
// whether the captcha must be deleted from the store.
func (r RedisStore) Get(id string, clear bool) string {
    key := CAPTCHA + id
    val, err := global.RedisDb.Get(ctx, key).Result()
    if err != nil {
        fmt.Println(err)
        return ""
    }
    if clear {
        err := global.RedisDb.Del(ctx, key).Err()
        if err != nil {
            fmt.Println(err)
            return ""
        }
    }
    return val
}

// Verify captcha's answer directly
func (r RedisStore) Verify(id, answer string, clear bool) bool {
    v := RedisStore{}.Get(id, clear)
    //fmt.Println("key:"+id+";value:"+v+";answer:"+answer)
    return v == answer
}

2.创建一个用于发送验证码到邮箱的utils工具(具体的邮箱配置忽略)

package utils

import (
    "context"
    "fmt"
    "github.com/jordan-wright/email"
    "github.com/mojocn/base64Captcha"
    "net/smtp"
)

// 发送邮箱验证码
func SendEmailValidate(em []string, myType int) (string, error) {
    e := email.NewEmail()
    e.From = fmt.Sprintf("发件人邮箱")
    e.To = em
    // 生成6位随机验证码
    driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)

    //这里在生成验证码对象时,就使用了redis作为验证码池
    cp := base64Captcha.NewCaptcha(driver, cache.RedisStore{})

    id, _, anwser, err := cp.Generate()
    fmt.Println("id", id)
    fmt.Println("anwser", anwser)
    if err != nil {
        fmt.Println("err")
        return "", err
    }
    ems := em[0]

    //为id添加验证码类型,1为注册,2为登录
    if myType == 1 {
        id = fmt.Sprintf("%s&amp%s", id, "emailtoregister")
    } else if myType == 2 {
        id = fmt.Sprintf("%s&amp%s", id, "emailtologin")
    } else {
        id = fmt.Sprintf("%s&amp%s", id, "emailtoforgetpassword")
    }
    cmd := global.RedisDb.Set(context.Background(), ems, id, 2*time.Minute)
    if cmd.Err() != nil {
        log.Fatal(cmd.Err())
        return "", cmd.Err()
    }
    //设置文件发送的内容
    content := fmt.Sprintf(`
    <div>
        <div>
            您好!
        </div>
        <div style="padding: 8px 40px 8px 50px;">
            <p>您提交的邮箱验证,本次验证码为<u><strong>%s</strong></u>,为了保证账号安全,验证码有效期为5分钟。请确认为本人操作,切勿向他人泄露,感谢您的理解与使用。</p>
        </div>
        <div>
            <p>此邮箱为系统邮箱,请勿回复。</p>
        </div>
    </div>
    `, anwser)
    e.Text = []byte(content)
    //设置服务器相关的配置
    err = e.Send("smtp.qq.com:25", smtp.PlainAuth("", "发件人邮箱", "发件人的密钥", "smtp.qq.com"))
    fmt.Println("to", e.To)
    return anwser, err
}

3.gin框架发送验证码的路由

// 发送注册验证码
func GetValidateCode(c *gin.Context) {
    // 获取目的邮箱
    ems := c.Param("email")
    temp := strings.Split(ems, "@")
    if len(temp) < 2 {
        c.JSON(http.StatusBadRequest, gin.H{
            "msg":  "请输入正确邮箱",
            "code": 400,
            "data": nil,
        })
        return
    }
    em := []string{ems}

    fmt.Println(ems)
    fmt.Println(em)

    //发送验证码,并将验证码id和email存入redis
    _, err := utils.SendEmailValidate(em, 1)
    if err != nil {
        log.Println(err)
        c.JSON(http.StatusBadRequest, gin.H{
            "status":           400,
            "msg":              "验证码发送失败",
            "ERROR-CONTROLLER": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "msg":    "验证码发送成功",
        "status": 200,
    })
    return
}

代码部分到此结束,接下来我要讲解以下验证码的校验思路,来看两个问题:

1.假定,现在恶意攻击者准备用别人的邮箱以爆破的方式登入别人的账号时,如果我们只是简单的对验证码做存储和校验会遇到什么问题呢?

        1)验证码池冗余:可以预见的一个问题是,如果攻击者不断向我们的后端发起请求验证码的请求,即便我们设置了过期时间,但是在大量请求的情况下验证码池还是会冗余大量的验证码。而在我们底层,只有base64Captcha自动生成的id和验证码做绑定。即便id的生成是随机的,但这也无疑增加了爆破成功的概率。

              为了解决这个问题,我们可以在id为键,存贮验证码的基础上再加一层,即邮箱为键,绑定id。这样一来,无论请求方如何请求我们的验证码,因为邮箱是不变的,且对于id来说是唯一的的,所以每次请求都会把上一次存储的id覆盖,也就意味着上一次请求的验证码是无效的。

        2)验证码越权:第二个问题,在上述问题中,我们使用了邮箱和id进行绑定。但是依旧存在问题,假设现在,一个用户在邮箱注册处获得了一个验证码,结果他用这个验证码去进行登录。这在业务上极其不合理,但是按照底层逻辑来说这是能实现的。因为邮箱是不变的,无论登录和注册,我获取验证码都是通过这一个邮箱号。

                为了解决这个问题,我选择的方法是:在原生的id后拼接标识符,以达到标识这个验证码对应的是哪一个接口的目的。也即id + 分隔符 + 标识符。这样一来,我只要通过分隔符切分,取到数组的最后一个,再在对应路由中,判断这个验证码是否为对应功能即可。具体验证方法如下:
// ValidateEmailCode
// @Title ValidateEmailCode
// @Description  验证邮箱验证码,并注册用户。
// @Author hyy 2022-03-05 18:19:18
// @Param c type description
func ValidateEmailCode(c *gin.Context) {

    //检验邮箱是否合法
    var user forms.CreateUserByEmail

    err := c.ShouldBindJSON(&user)
    fmt.Println(user)
    if err != nil {
        log.Println(err.Error())
        c.JSON(http.StatusBadRequest, gin.H{
            "status":           400,
            "msg":              "注册失败,json解析失败",
            "ERROR-CONTROLLER": err.Error(),
        })
        return
    }
    myEmail := user.Email
    // 默认用户权限为2
    if user.RoleId == 0 {
        user.RoleId = 3
    }

    // 通过邮箱获取在redis中绑定的id
    id, err := global.RedisDb.Get(context.Background(), myEmail).Result()

    //分隔符切分
    myResp := strings.Split(id, "&amp")

    if len(myResp) < 2 {
        c.JSON(http.StatusInternalServerError, gin.H{
            "msg":  "验证码失效",
            "code": 500,
            "data": nil,
        })
        return
    }

    //判断接口是否对应
    if myResp[1] != "emailtoregister" {
        c.JSON(http.StatusBadRequest, gin.H{
            "msg":  "验证码失效",
            "data": nil,
            "code": 400,
        })
        return
    }

    id = myResp[0]
    if err != nil {
        log.Println(err.Error())
        c.JSON(http.StatusBadRequest, gin.H{
            "status":           400,
            "msg":              "Redis获取vCode失败",
            "ERROR-CONTROLLER": err.Error(),
        })
        return
    }
标签: 安全

本文转载自: https://blog.csdn.net/qq_62906402/article/details/139880144
版权归原作者 桐生战兔868 所有, 如有侵权,请联系我们删除。

“golang gin base64Captcha生成验证码后通过redis存储 以及安全验证思路”的评论:

还没有评论