统计
  • 建站日期:2022-01-17
  • 文章总数:3860 篇
  • 评论总数:10756条
  • 分类总数:43 个
  • 最后更新:今天

短信验证码登录的思路和简单实现

首页 综合教程 正文
广告
广告
广告
广告

本次笔记要记录的是短信验证码登录功能的实现,使用lua脚本简化验证登录的流程,以及使用阿里云短信服务实现真实的发送短信验证码登录的功能。

短信验证码登录

概述

使用短信验证码是常见的登录功能,对于用户来说使用短信验证码登录的流程一般是在网站输入手机号,点击获取验证码,输入验证码通过验证后登录成功。对于后端服务来说有几件事要注意的,首先是收到用户获取验证码的请求时记录次数,在一段时间内限制请求次数(如10分钟内5次请求);然后是验证验证码的流程,发送短信后将用户手机号和验证码存储起来并设置过期时间(如5分钟内有效);最后是当验证通过后需要将验证码删除或设置过期,若用户该手机号未注册则自动注册。

思路

对于用户来说,短信验证码登录的流程一般是这样的:用户在登录界面输入手机号点击获取验证码,用户收到短信后输入验证码,若验证码正确则登录成功,若手机号未注册则自动注册(一般都有自动注册协议让用户勾选)。

image.png

对于后台服务来说,短信验证码的发送登录的流程一般是这样的:后台服务收到发送验证码的请求,查询该手机号是否还有发送短信的剩余次数,若可以发送则生成随机数调用SMS服务发送验证码,同时记录该手机号在某个时间范围内的请求次数,存储该用户的手机号和验证码。

image.png

验证验证码的流程一般是这样的:

image.png

当后端服务收到验证验证码登录请求时,在刚才存储的数据中验证验证码是否正确。如果验证码正确,用户手机号未注册则给用户自动注册。若验证码错误或过期则返回登录失败。
在本次学习笔记中后端接收到发送验证码的请求时会生成6位的随机数,并将手机发送短信剩余次数和手机号及对应验证码保存到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
        }
    }
}

image.png

验证验证码登录的请求

在这里我们要做几件事,首先是根据请求中的phonecodeRedis中存储的值比对。如果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
}

image.png

使用Lua脚本优化验证流程

概述

在短信验证码登录功能中,我们需要频繁操作redis数据库并根据返回的数据判断下一步的逻辑。例如在发送验证码服务中,首先要查询在redis中查询是否有key存储短信发送剩余次数,没有该key则操作redis数据库设置该手机号的剩余次数;若有该key则查询redis数据库剩余次数,若value大于0表示还能发送短信,否则取消发送。同时发送后还要将剩余次数减一。可以看到控制流程还是挺复杂的,在程序里面要写不少if else语句,这时候我们可以使用lua脚本帮助我们做这些判断。
Lua是一种轻量级、可拓展的脚本语言,常用于Web开发、游戏开发、嵌入式等领域。在这个项目中我们使用go:embedlua脚本嵌入到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
   end

4. 测试。这里使用v2路径表示使用了lua脚本。

image.png

使用阿里云SMS服务

概述

在阿里云短信服务那有个快速使用流程,需要实名认证、开通短信服务和获取AccessKey。生成的AccessKeyAccessSecret只会显示一次所以要保存好。把上面的流程完成后,拷贝一份提供的实例,填入必填项就能跑起来了。因为官网文档已经写得很详细了,各个流程也很清晰所以也没有记录的必要了

image.png

版权说明
文章采用: 《署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权。
版权声明:本站资源来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系客服并出示版权证明以便删除!
暗网市场:BriansClub
« 上一篇 02-07
最可怕的搜索引擎,可以搜索到你的摄像头!
下一篇 » 02-07

发表评论

  • 泡泡
  • 阿呆
  • 阿鲁
  • 蛆音娘
    没有更多评论了