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
	}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值