引言
当前很多web端的应用登录方式主要分为以下几种:
- 账号密码登录
- 手机号验证码登录
- 扫码登录
这里我主要说一下我对于手机号验证码登录的思路,如果有遗漏或者差错的地方,请指正;
整体流程
大致流程如下:
大致就是这样,其中一些细节并没有体现出来,下面我用代码演示具体操作步骤;
获取验证码接口
用户输入手机号首先肯定是要获取验证码,所以先要实现获取验证码接口;
具体代码:
@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次)// 该手机号短信发送次数+1long 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);// 生成tokenString token = JwtUtil.createJWT(user.getId().toString());// 将用户信息存入redisredisTemplate.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赋为0time = 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 = 60this.codeLoading = true // 禁止点击发送验证码按钮this.countDown(time) // 开始倒计时} else {this.$message.error(res.description)}}).catch(res => {this.$message.error(res.message)})}
},
这样就能实现获取验证码按钮倒计时了,但是当按钮倒计时时刷新界面后倒计时会重置,网上思路是可以把倒计时存到cookie中,但是如果删除cookie还是会重置;但是虽然重置了按钮,但是后端的60s频率限制计时不会重置,所以依然不能发送验证码,可以避免别有用心之人恶意调用接口浪费你的短信服务费用。
效果查看
最后查看一下总体效果吧;
获取验证码
redis缓存情况
短时间内再次发送
多次发送超过今日总上限
对应的redis缓存,超出今日的10次
正常登录成功
总结
我在实现验证码登录过程中感觉有些复杂的地方是验证码的发送频率限制,两次限制的实现方式;其他就没有那么难了;当然我的思路可能会存在一些问题,如果有希望指点一二;