手机号验证码登录的思路

引言

当前很多web端的应用登录方式主要分为以下几种:

  • 账号密码登录
  • 手机号验证码登录
  • 扫码登录

这里我主要说一下我对于手机号验证码登录的思路,如果有遗漏或者差错的地方,请指正;

整体流程

大致流程如下:

image-20230212110731298

大致就是这样,其中一些细节并没有体现出来,下面我用代码演示具体操作步骤;

获取验证码接口

用户输入手机号首先肯定是要获取验证码,所以先要实现获取验证码接口;

具体代码:

@PostMapping("/tencent/code/{phone}")
public BaseResponse<String> tencentSendMessageToPhone(@PathVariable String phone) {
    // 校验信息
    this.verifyPhoneInfo(phone);
    // 如果redis没有该手机号验证码,则获取验证码并发送短信
    String verifyCode = RandomSmsNumUtils.getSixBitRandom(); // 获取六位验证码
    Boolean isSend = smsService.tencentSendMessageToPhone(verifyCode, phone); // 调用tencent短信发送sdk
    // 判断发送结果并处理
    this.afterMessageSending(isSend, phone, verifyCode);
    return ResultUtils.success("短信发送成功");
}

 /**
     * 校验发送验证码手机号信息
     * @param phone 手机号
     */
    private void verifyPhoneInfo(String phone) {
        if (StringUtils.isAnyBlank(phone)) {
            throw new BusinessException(StatusCode.NULL_ERROR, "手机号为空");
        }
        // 校验手机号
        RegExpUtil.regExpVerify(RegExpUtil.phoneRegExp, phone, "手机号格式错误");
        // 判断短时间内是否重复发送验证码(限制发送频率60S,60s内只同一个手机号能发送1次)
        // 从redis中查看有没有该手机号的验证码
        String verifyCode = (String) redisTemplate.opsForValue().get(RedisKey.SMS_LOGIN_CODE + phone);
        if (!StringUtils.isAnyBlank(verifyCode)) {
            long codeCreateTime = Long.parseLong(verifyCode.split("_")[1]); // 获取验证码创建时间
            if (System.currentTimeMillis() - codeCreateTime < 60000) { // 如果发送间隔小于60秒,则禁止再次发送
                throw new BusinessException(StatusCode.SMS_CODE_ERROR, "短信发送频繁,请稍后操作");
            }
        }
        // 判断今日发送验证码总次数(限制发送次数10次,24h内只同一个手机号能发送10次)
        // 该手机号短信发送次数+1
        long count = redisTemplate.opsForValue().increment(RedisKey.SMS_LIMIT_NUM + phone, 1);
        if (count == 1) { // count发送次数为一说明该手机号用户今天第一次调用接口发送短信,则开始24小时倒计时
            redisTemplate.expire(RedisKey.SMS_LIMIT_NUM + phone, 1, TimeUnit.DAYS);
        }
        if (count > 10) { // count发送次数大于10次则该手机号超出今日短信发送上线,禁止发送
            throw new BusinessException(StatusCode.SMS_CODE_ERROR, "今日短信发送次数超出上线");
        }
    }

    /**
     * 判断发送结果并处理
     * @param isSend 发送结果
     * @param phone 发送手机号
     * @param verifyCode 验证码
     */
    private void afterMessageSending(Boolean isSend, String phone, String verifyCode) {
        if (isSend) {
            // 如果发送成功,则将对应手机号验证码存入redis中,设置规定时间内有效
            redisTemplate.opsForValue().set(
                    RedisKey.SMS_LOGIN_CODE + phone,
                    verifyCode + "_" + System.currentTimeMillis(), // 验证码后加上该验证码创建时间,用来判断该验证码是否是60s内发送的
                    MESSAGE_EXPIRED_TIME,
                    TimeUnit.MINUTES);
        } else {
            throw new BusinessException(StatusCode.SYSTEM_ERROR, "短信发送失败");
        }
    }

这里需要用到两个redis的key,一个是存验证码+验证码创建时间,用来判断验证码正确性和限制短时间内的发送频率;另一个是存该手机号今日的发送次数,用来限制一天内该手机号发送次数的。

service层调用腾讯云sdk的代码就不演示了,具体的可以看这篇文章:腾讯云短信服务——获取验证码

获取验证码接口就完成了;

手机号验证码登录接口

用户输入验证码后点击登录所调用的接口,思路比较简单,代码如下:

controller

// 手机号验证码登录接口
@PostMapping("/phone")
public BaseResponse<LoginVo> phoneCodeLogin(@RequestBody PhoneCodeLoginRequest loginRequest) {
    if (loginRequest == null || StringUtils.isAnyBlank(loginRequest.getPhone(), loginRequest.getCode())) {
        throw new BusinessException(StatusCode.PARAMS_ERROR);
    }
    String phone = loginRequest.getPhone();
    String code = loginRequest.getCode();
    LoginVo loginVo = userService.phoneCodeLogin(phone, code);
    return ResultUtils.success(loginVo);
}

serviceImpl

@Override
@Transactional
public LoginVo phoneCodeLogin(String phone, String code) {
    // 参数校验
    if (StringUtils.isAnyBlank(phone, code)) {
        throw new BusinessException(StatusCode.PARAMS_ERROR, "参数为空");
    }
    RegExpUtil.regExpVerify(RegExpUtil.phoneRegExp, phone, "手机号格式错误");
    // 从redis中获取验证码进行校验
    String phoneCode = (String) redisTemplate.opsForValue().get(RedisKey.SMS_LOGIN_CODE + phone);
    if (StringUtils.isAnyBlank(phoneCode)) {
        throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码不存在或已超时");
    }
    phoneCode = phoneCode.split("_")[0]; // 获取真正的验证码
    if (!code.equals(phoneCode)) {
        throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码错误");
    }
    // 判断该用户是否存在,若不存在,则注册为新用户
    User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
    if (user == null) { // 注册为新用户
        user = new User();
        user.setPhone(phone);
        user.setAvatar(ApiUtils.getRandomAvatar());
        user.setProfile("简单介绍一下自己吧!");
        user.setNickname(RandomNameUtils.randomName(true, 3));
        userMapper.insert(user);
        // 重新获取新增用户信息
        user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
        // 设置用户为普通用户
        Role normalRole = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleName, "普通用户"));
        UserRoleRelation userRoleRelation = new UserRoleRelation();
        userRoleRelation.setUserId(user.getId());
        userRoleRelation.setRoleId(normalRole.getId());
        userRoleRelationService.save(userRoleRelation);
    }
    // 用户存在(或已注册成功)进行登录操作
    // 封装用户信息
    UserVo userVo = setUserVo(user);
    // 生成token
    String token = JwtUtil.createJWT(user.getId().toString());
    // 将用户信息存入redis
    redisTemplate.opsForValue().set(RedisKey.LOGIN_USER + user.getId(), userVo, 14, TimeUnit.DAYS);
    // 登录成功后将验证码清除
    redisTemplate.delete(RedisKey.SMS_LOGIN_CODE + phone);
    // 返回信息
    return new LoginVo(userVo, token);
}

代码并不难,登录流程清楚了就能看懂了;

前端代码

这里主要想说前端如何做验证码倒计时,我简单用js实现了一下,通过setTimeout进行递归调用即可。

countDown(time) {
  if (this.codeLoading === false) { // 如果不需要倒计时加载时,time赋为0
    time = 0
  }
  console.log(time)
  if (time === 0 ) {
    this.codeContext = '重新获取'
    this.codeLoading = false
  } else {
    this.codeContext = time + '秒后重新获取'
    time--
    setTimeout(() => { // 递归countDown函数倒计时
      this.countDown(time)
    }, 1000)
  }
},
// 获取手机号登录验证码
getPhoneCode() {
  if (!this.phoneLoginForm.phone) {
    this.$message.warning('请输入手机号')
  }
  else {
    // 发送获取验证码请求
    getPhoneCodeByAliyun(this.phoneLoginForm.phone).then(res => {
      if (res.code === 20000) {
        this.$message.success('验证码发送成功')
        let time = 60
        this.codeLoading = true // 禁止点击发送验证码按钮
        this.countDown(time) // 开始倒计时
      } else {
        this.$message.error(res.description)
      }
    }).catch(res => {
      this.$message.error(res.message)
    })
  }
},

这样就能实现获取验证码按钮倒计时了,但是当按钮倒计时时刷新界面后倒计时会重置,网上思路是可以把倒计时存到cookie中,但是如果删除cookie还是会重置;但是虽然重置了按钮,但是后端的60s频率限制计时不会重置,所以依然不能发送验证码,可以避免别有用心之人恶意调用接口浪费你的短信服务费用。

效果查看

最后查看一下总体效果吧;

image-20230212112809577

获取验证码

image-20230212112849262

redis缓存情况

image-20230212113155286

短时间内再次发送

image-20230212113101213

多次发送超过今日总上限

image-20230212113303432

对应的redis缓存,超出今日的10次

image-20230212113355885

正常登录成功

image-20230212113557452

总结

我在实现验证码登录过程中感觉有些复杂的地方是验证码的发送频率限制,两次限制的实现方式;其他就没有那么难了;当然我的思路可能会存在一些问题,如果有希望指点一二;

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
下面是一些测试用例示例,用于测试手机号获取验证码的功能: 1. 测试有效手机号码能否成功获取验证码: - 输入:有效的手机号码(例如:13812345678) - 预期输出:成功获取验证码,并返回验证码信息。 2. 测试无效手机号码是否能够正确处理: - 输入:无效的手机号码(例如:12345678901) - 预期输出:返回错误提示信息,提示手机号码无效。 3. 测试手机号码为空时是否能够正确处理: - 输入:空的手机号码 - 预期输出:返回错误提示信息,提示手机号码不能为空。 4. 测试已被注册的手机号码是否能够正确处理: - 输入:已被注册的手机号码(例如:已经在系统中注册过的手机号码) - 预期输出:返回错误提示信息,提示手机号码已被注册。 5. 测试短信验证码是否正确生成: - 输入:有效的手机号码 - 预期输出:成功获取验证码,并返回正确的验证码信息。 6. 测试验证码有效期是否正确: - 输入:有效的手机号码 - 预期输出:成功获取验证码,并检查验证码的有效期是否符合要求。 7. 测试频繁获取验证码是否正确限制: - 输入:在短时间内多次连续获取验证码 - 预期输出:检查系统是否正确限制了频繁获取验证码的操作,并返回相应的错误提示信息。 这些测试用例覆盖了常见的手机号获取验证码的情况,可以帮助你确保该功能的正确性和稳定性。根据具体的系统要求和业务逻辑,你可以进一步扩展和调整这些测试用例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YXXYX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值