概述
lua
脚本简化验证登录的流程,以及使用阿里云短信服务实现真实的发送短信验证码登录的功能。短信验证码登录
概述
思路
验证验证码的流程一般是这样的:
redis
数据库中。这次我们将用户请求次数限制在每10分钟5次发送验证码请求,每次请求都在redis
数据库中将该手机号的剩余发送此处减1
,如果10分钟内超过5次请求则将剩余次数置为-1
,不发送短信;将用户的验证码过期时间设置在5分钟,验证码在验证通过后要被删除(验证码只能用一次),验证码过期后在redis
中自动删除。当使用验证码通过校验后查询数据库中的用户,若用户未注册则给用户自动注册,通过用户id
生成jwt token
返回给前端。步骤
发送验证码的请求
controller
层我们需要做三件事,一是查询用户发送短信的剩余次数,二是调用发送验证码的方法,最后将验证码存储起来。// 在controller层
// 请求发送验证码
func HandlerSendSMSForLogin(ctx *gin.Context) {
var fo *models.SMS
if err := ctx.ShouldBindJSON(&fo); err != nil {
zap.L().Error("Sign In with invalid params", zap.Error(err))
ctx.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "bad request",
})
return
}
// 检查该用户发送短信的剩余次数
status, err := cache.CheckSMSResidualDegree(fo.Phone)
if err != nil {
log.Println("检查短信错误", err)
return
}
if status != true {
log.Println("发送短信验证码次数用完")
ctx.JSON(http.StatusTooManyRequests, gin.H{
"code": http.StatusTooManyRequests,
"msg": "操作过于频繁,请稍后再试",
})
return
}
// 没有问题的话发送验证码,交给Logic层处理
code, err := logic.SMSLogin(fo.Phone)
if err != nil {
ctx.JSON(http.StatusNotFound, gin.H{
"code": 404,
"msg": "send code error",
})
}
// 发送验证码没问题,将验证码存储到redis中,设置过期时间5分钟
if err = cache.SetCodeForUserSMSLogin(fo.Phone, code); err != nil {
log.Println("set code for user sms in redis error", err)
// 如果redis写不进去,就要写进其他数据库或本地存储
}
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "send sms success",
})
}
- - - - - - - - - - - - 分割线 - - - - - - -
// 在redis中检查短信剩余次数的函数
func CheckSMSResidualDegree(phone string) (status bool, err error) {
/*
1.首先查询key是否存在,如果存在就不能覆盖
2.key如果存在,则查询value剩余次数并 -1
3.key如果不存在则添加一个(10分钟前没有请求过)并设置剩余次数5次,过期时间600s
4.查询该手机发送验证码的的剩余次数,如果 -1次 大于0,那么可以发送
*/
tempKey := fmt.Sprintf("%s%s", KeyUserSMSCount, phone)
if existed := ExistKey(tempKey); existed != true {
// key不存在 设置值
if err = rdb.Set(ctx, tempKey, 5, time.Second*600).Err(); err != nil {
// 操作redis失败
return false, err
} else {
return true, nil
}
} else {
// key 存在 查询值
res, err := rdb.Get(ctx, tempKey).Result()
if err != nil {
// 查询的时候出错 返回错误
return false, err
}
intres, _ := strconv.Atoi(res)
if intres-1 > 0 {
// 还有剩余次数 -1 并返回true
if err = rdb.Decr(ctx, tempKey).Err(); err != nil {
// 操作redis错误
return false, err
}
return true, nil
} else {
/*
获取key的ttl剩余时间,并将value置为 -1
*/
ttl, err := rdb.TTL(ctx, tempKey).Result()
if err != nil {
return false, err
}
err = rdb.Set(ctx, tempKey, -1, ttl).Err()
if err != nil {
return false, err
}
return false, nil
}
}
}
验证验证码登录的请求
在这里我们要做几件事,首先是根据请求中的 phone
和code
与Redis
中存储的值比对。如果Redis中没有key
或对应的值对不上,那么登录失败。如果验证正确那么下一步去查询是否存在该用户,如果不存在则帮助用户注册,登录成功返回jwt token
。如果用户存在则登录成功直接返回jwt token
。/*
处理使用验证码登录的请求
*/
func HandlerUserSMSLogin(ctx *gin.Context) {
var fo *models.VerifySMSLogin
if err := ctx.ShouldBindJSON(&fo); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "请求参数错误",
})
ctx.Abort()
}
/*
在redis中验证,从redis中取出验证码,此时会有两种情况:
1. redis中没有这个key
2. redis中key对应的value不正确
*/
key, verify, err := cache.VerifyCodeForUserSMSLogin(fo.Phone, fo.Code)
if err != nil {
fmt.Println("系统化错误", err)
ctx.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "系统错误",
})
}
user, err := logic.GetUserProfileByPhone(fo.Phone)
if err != nil {
fmt.Println("查询Mysql数据库错误")
}
if user.Phone == "" {
// 创建用户
if err := logic.CreateUserByPhone(fo.Phone); err != nil {
fmt.Println("创建用户失败 返回系统错误")
ctx.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"msg": "系统错误",
})
ctx.Abort()
}
}
// 如果验证码正确且用户手机不为空
if verify == true && user.Phone != "" {
strToken, _ := JWT.GenToken(user.Id)
fmt.Println("SMS验证通过,清除redis中的sms cache")
_ = cache.DeleteKey(key)
ctx.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "登录成功",
"token": strToken,
})
} else if verify == false {
ctx.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "登录失败,请重试",
})
}
}- - - - - - - - - 分割线 - - - - - - -
// 在redis中 用于验证短信登录,手机号和验证码是否正确
func VerifyCodeForUserSMSLogin(phone, code string) (key string, res bool, err error) {
key = fmt.Sprintf("%s%s", KeyUserSMSLoginSet, phone)
val, err := rdb.Get(ctx, key).Result()
if err != nil {
// 使用Go操作redis数据库时如果尝试获取一个不存在的key,那么会返回一个redis.Nil的错误
if errors.Is(err, redis.Nil) {
return key, false, nil
}
fmt.Println("Key不存在", err)
return key, false, err
}
if val != code {
fmt.Println("Code ERROR 验证码不正确,登录失败")
return key, false, nil
}
return key, true, nil
}
使用Lua脚本优化验证流程
概述
在短信验证码登录功能中,我们需要频繁操作 redis
数据库并根据返回的数据判断下一步的逻辑。例如在发送验证码服务中,首先要查询在redis
中查询是否有key
存储短信发送剩余次数,没有该key则操作redis
数据库设置该手机号的剩余次数;若有该key则查询redis
数据库剩余次数,若value
大于0表示还能发送短信,否则取消发送。同时发送后还要将剩余次数减一。可以看到控制流程还是挺复杂的,在程序里面要写不少if else
语句,这时候我们可以使用lua
脚本帮助我们做这些判断。Lua
是一种轻量级、可拓展的脚本语言,常用于Web开发、游戏开发、嵌入式等领域。在这个项目中我们使用go:embed
将lua
脚本嵌入到Go的二进制文件中,并使用redis
提供的EVAL
方法执行Lua
脚本,同时Go项目上使用Lua
语言需要使用库github.com/yuin/gopher-lua
。思路
在短信验证码登录功能实现的过程中,主要有三部分使用 lua
脚本处理逻辑关系,第一部分是发送验证码时获取短信发送的剩余次数,根据剩余次数返回查询情况,在剩余次数有效的情况下将次数减一。第二部分是短信验证码发出去时将手机号和验证码存储在redis
数据库中。第三部分是用于验证验证码是否有效;若验证码有效则要将对应的key
删除或设置过期,并返回查询情况步骤
1. 编写查询
redis
数据库中验证码剩余次数的功能lua
脚本。在lua
脚本中KEYS
是一个全局变量,它是一个数组,用于存储Redis
键的名称,可以通过KEYS[1]
这个方法来获取对应的键。ARGV
也是一个全局变量,也是一个数组,用于存储传递给脚本的参数值,同样使用ARGV[1]
的方式访问。下面是查询验证码次数功能的
lua
脚本,以及一个执行lua
脚本的函数。local key = KEYS[1] --获取传递过来的KEY
local countkey = "CACHE/users/smscount/" .. key -- 拼接字符串
local existed = redis.call("get", countkey) -- 查询这个key是否存在
local ttl = tonumber(redis.call("ttl", countkey)) --获取过期时间
if existed ~= false then -- 如果key存在的情况
local count = redis.call('decr', countkey) -- 剩余次数-1
if count - 1 > 0 then -- 如果还有剩余次数
return 1 -- 约定返回1可以发送短信验证码
else
redis.call("set", countkey, -1) -- 没有剩余次数了,将剩余次数置为-1
redis.call("expire", countkey, ttl) -- 按照之前的过期时间设置为之前的值
return -1 -- 约定返回-1不发送验证码
end
else
redis.call("set", countkey, 5) -- 没有这个key,设置key和剩余次数5次
redis.call("expire", countkey, 600) -- 设置过期时间是600s
return 1 -- 约定返回1可以发送验证码
end//controller中处理发送验证码请求
func HandlerUserSMSForLoginV2(ctx *gin.Context){
...
// 使用lua脚本
result, err := cache.EvalLuaScript(fo.Phone, "", luaSMS)
if err != nil {
log.Println("Eval Lua Script ERROR", err)
ctx.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "系统错误",
})
ctx.Abort()
return
}
...
}
- - - - - - - - - -
// 执行lua脚本的函数
func EvalLuaScript(key string, value string, luaScript string) (interface{}, error) {
result, err := rdb.Eval(ctx, luaScript, []string{key}, value).Result()
if err != nil {
return nil, err
}
return result, nil
}2. 编写写入
redis
数据库手机号和验证码的lua
脚本local key = KEYS[1] -- 获取传过来的key
local setKey = "CACHE/users/sms/" .. key --拼接字符串
local value = ARGV[1] -- 获取传过来的value
redis.call("set", setKey, value) -- set key value
redis.call("expire", setKey, 600) -- 设置验证码过期时间600秒
return 1 -- 约定返回1表示成功3. 编写验证
redis
数据库中存储的手机号对应验证码的lua
脚本local key = KEYS[1]
local setKey = "CACHE/users/sms/" .. key
local expectCode = ARGV[1]
local existed = redis.call("get", setKey)
if existed ~= false then
local verify = redis.call("get", setKey)
if verify == expectCode then
redis.call("del", setKey)
return 1
else
return -1
end
else
return -1
end4. 测试。这里使用
v2
路径表示使用了lua
脚本。
使用阿里云SMS服务
概述
在阿里云短信服务那有个快速使用流程,需要实名认证、开通短信服务和获取 AccessKey
。生成的AccessKey
和AccessSecret
只会显示一次所以要保存好。把上面的流程完成后,拷贝一份提供的实例,填入必填项
就能跑起来了。因为官网文档已经写得很详细了,各个流程也很清晰所以也没有记录的必要了。
写在最后
本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正。谢谢!练习的代码放在这里–↓ https://github.com/FengZeHe/LearnGolang/tree/main/project/BasicProject
原文始发于微信公众号(ProgrammerHe):短信验证码登录的思路和简单实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/207861.html