Java实现短信验证码

大家好,我是孙嵓,短信验证码相信大家都不陌生吗,但是短信验证码怎么生成的你真的了解吗,本文揭示本人项目中对短信验证码的。

项目需求

用户注册/忘记密码添加短信验证码

需求来由

登录注册页面需要确保用户同一个手机号只关联一个账号确保非人为操作,避免系统用户信息紊乱增加系统安全性

代码实现

同事提供了WebService接口,很好,之前没调过,又增加了困难。

这边用的阿里云的短信服务,废话少说上图,呸,上代码—

发送验证码方法

public AjaxResult sendVerificationCode(LoginBody loginBody) {
    //拼装redis的key
    String redisCodeKey = Constants.RECRUIT_CODE_KEY + loginBody.getUserName();
    //通过判断过期时间检验是否发送过验证码如果发送直接return
    if (redisCache.getExpire(redisCodeKey) >= 0) {
        return AjaxResult.error(TipsConstants.YZM_SEND_ALREADY);
    }
    //生成随机6位验证码
    String redisCodeValue = VerifyCodeUtils.generateSmsCode();
    //验证码类型这是根据同事给的webservice的文档单独封装的目前先这么写了;判断其是注册还是忘记密码
    VerificationCodeType verificationCodeType = VerificationCodeType.getByCode(loginBody.getVerificationCodeType());
    String templateCode = null;
    switch (verificationCodeType) {
        case REGISTER:
            templateCode = VerificationCodeType.REGISTER.getCode();
            break;
        case FORGET_PASSWORD:
            templateCode = VerificationCodeType.FORGET_PASSWORD.getCode();
            break;
        default:
            break;
    }
    //webservice接口需要json格式的参数
    JSONObject jsonObject = new JSONObject();
    jsonObject.put(WebServiceConstants.CODE, redisCodeValue);
    Map<String, String> resultMap = SMSUtils.sendMessage(loginBody.getUserName(),templateCode,jsonObject);
    //判断webservice接口返回的结果
    if (!resultMap.get(WebServiceConstants.SEND_SMS_RESULT).equals(Constants.SUCCESS)) {
        logger.info(resultMap.get(WebServiceConstants.OUT_MSG));
        logger.info(resultMap.get(WebServiceConstants.BIZ_ID));
        return AjaxResult.error(TipsConstants.MSG_SERVER_ERROR);
    }
    //存储到redis设置过期时间,这里设置了60s,根据需求来
    redisCache.setCacheObject(redisCodeKey, redisCodeValue, 60, TimeUnit.SECONDS);
    return AjaxResult.success();
}

注册方法

public AjaxResult register(LoginBody loginBody) {
    //拼装redis key
    String redisCodeKey = Constants.RECRUIT_CODE_KEY + loginBody.getUserName();
    //redisCache封装了redis的方法;
    //获取验证码判断验证码是否为空;输入的验证码与短信验证码是否一致
    String redisCodeValue = redisCache.getCacheObject(redisCodeKey);
    if (StringUtils.isEmpty(redisCodeValue) || !loginBody.getVerificationCode().equals(redisCodeValue)) {
        return AjaxResult.error(TipsConstants.YZM_ERROR);
    }
    //查表校验用户是否注册
    SysUser existUser = sysUserMapper.checkPhoneUnique(loginBody.getUserName());
    if (!ObjectUtil.isEmpty(existUser)) {
        return AjaxResult.error(TipsConstants.EXIST_USER_ERROR);
    }
    //对象copy,创建SysUser对象
    SysUser sysUser = BeanUtil.copyProperties(loginBody, SysUser.class, UserConstants.PASSWORD);
    sysUser.setPassword(SecurityUtils.encryptPassword(loginBody.getPassword()));
    //插入用户信息
    sysUserMapper.insertUser(sysUser);
    return AjaxResult.success(TipsConstants.REGISTER_SUCCESS);
}

忘记密码

public AjaxResult forgetPwd(LoginBody loginBody) {
    //拼装redis的key
    String redisCodeKey = Constants.RECRUIT_CODE_KEY + loginBody.getUserName();
    //获取验证码
    String redisCodeValue = redisCache.getCacheObject(redisCodeKey);
    if (!loginBody.getVerificationCode().equals(redisCodeValue)) {
        return AjaxResult.error(TipsConstants.YZM_ERROR);
    }
    //查表查询用户是否存在
    SysUser sysUser = sysUserMapper.checkPhoneUnique(loginBody.getUserName());
    if (ObjectUtil.isEmpty(sysUser)) {
        return AjaxResult.error(TipsConstants.NO_USER);
    }
    //密码加密
    loginBody.setPassword(SecurityUtils.encryptPassword(loginBody.getPassword()));
    //重置密码
    sysUserMapper.resetUserPwd(loginBody.getUserName(), loginBody.getPassword());
    return AjaxResult.success();
}

前端代码

这里只粘贴了发送验证码改变按钮的方法

sendCode(type) {
  this.$refs.registerForm.validateField('phone',(phoneError)=> {
    if(!phoneError){
      this.registerForm.verificationCodeType = type
      //短信验证码最大请求次数校验
      getSmsCode(this.registerForm).then(response => {
        if (response.code !== 200) {
          this.requestMax = true
        } else {
          this.msgSuccess('发送成功,请注意查收短信')
          this.requestMax = false
        }
        //发送验证码按钮修改
        if (!this.requestMax) {
          let time = 60
          this.buttonText = '已发送'
          this.isDisabled = true
          if (this.flag) {
            this.flag = false
            let timer = setInterval(() => {
              time--
              this.buttonText = time + ' 秒'
              if (time === 0) {
                clearInterval(timer)
                this.buttonText = '重新获取'
                this.isDisabled = false
                this.flag = true
              }
            }, 1000)
          }
        }
      })
    }
  })
},

编码中遇到的问题

1.webservice如何调用?
一开始导了很多关于webservice的相关依赖,结果掉不通没办法只能用Hutool了,send返回的是一个xml,再用documet将其解析就ok了。

SoapClient soapClient = SoapClient.create(WebServiceConfig.getMsgUrl())
        .setMethod(WebServiceMethod.SendSms.getCode(), WebServiceConfig.getNamespaceUri())
        .setParams(map, false);
String result = soapClient.send()

2.不能让用户无限制的请求发送验证码
据说短信平台有验证逻辑,为了安全还是给系统封了一层;这里通过注解,aop配合redis计数器进行最大请求次数验证。

代码如下
注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckRequestTimes {
    /**
     * 最大请求次数
     */
    String maxTimes() default "10";
    /**
     * 整个系统最大请求次数
     */
    String maxSystermTimes() default "1000";
    /**
     * 请求类型
     */
    RequestEnums reqType() default RequestEnums.COMMON;
    /**
     * 请求次数上限错误信息提示
     */
    String errorMsg() default TipsConstants.REQUEST_TIMES_MAX
}

Aspect

这部分代码我个人认为设计比较巧妙,可供读者思考,多利用设计模式思想去开发代码,让代码更优雅、更健壮、更可用,crud也有编出自己的骨气!!!(本实例涵盖了单例,模板方法)

@Aspect
@Component
@Order(2)
public class CheckRequestAspect {

    @Autowired
    RedisService redisService;
    @Autowired
    TokenService tokenService;

    private static Logger logger = LoggerFactory.getLogger(CheckRequestAspect.class);
    //防止并发,添加关键字实现共享
    private volatile ConcurrentHashMap<RequestEnums, RequestTimesAbstract> reqTimesProcessMap;
    
    @PostConstruct
    public void initExcelProcessorFactory() {
        //dcl 双重检查锁,也可进行懒散加载。因为现在基于spring容器单例,此锁可适当调整
        if (MapUtil.isNotEmpty(reqTimesProcessMap)) {
            return;
        }
        //眼熟不这叫懒汉式单例
        synchronized (this) {
            if (ObjectUtil.isNull(reqTimesProcessMap)) {
                reqTimesProcessMap = new ConcurrentHashMap(8);
            }
            //这里其实可以采用工厂方法去改造,由于业务没有太多类型所以就不设计工厂了
            reqTimesProcessMap.put(RequestEnums.COMMON, new UserCommReqTimes());
            reqTimesProcessMap.put(RequestEnums.SMS, new SMSCodeReqTimes());
        }
    }
    /**
     * 切入点
     */
    @Pointcut("@annotation(com.fuwai.hr.common.annotation.CheckRequestTimes)")
    public void checkPoint() {

    }
    /**
     * 环绕获取请求参数
     *
     * @param proceedingJoinPoint
     * @return
     */
    @Around("checkPoint()")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
        //获取方法上的注解
        CheckRequestTimes checkRequestTimes = getAnnotation(proceedingJoinPoint);
        Object[] args = proceedingJoinPoint.getArgs();
        //判断是否到达最大请求次数,这里为了应对不同请求类型的处理方式写了一个抽象类,
        //便于扩展维护,沿用了了模板方法设计模式的思想
        if(!reqTimesProcessMap.get(checkRequestTimes.reqType()).judgeMaxTimes(args, checkRequestTimes, redisService)){
            return AjaxResult.error(HttpStatus.REQUEST_MAX, checkRequestTimes.errorMsg());
        }
        //执行请求方法
        Object proceed = null;
        try {
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            logger.error(throwable.getMessage(), throwable);
        }
        return proceed;
    }
    /**
     * 获取方法上的注解以便拿到对应的值
     *
     * @param proceedingJoinPoint
     * @return
     */
    private CheckRequestTimes getAnnotation(ProceedingJoinPoint proceedingJoinPoint) {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        if (method != null){
            return method.getAnnotation(CheckRequestTimes.class);
        }
        return null;
    }
}

抽象模板类

public abstract class RequestTimesAbstract {
    /**
     * 判断是否到达请求最大次数
     * @param object 参数
     * @param checkRequestTimes  注解
     * @param redisService   redis服务
     * @return
     */
    public abstract boolean judgeMaxTimes(Object object, CheckRequestTimes checkRequestTimes, RedisService redisService);
}

短信模板子类

public class SMSCodeReqTimes extends RequestTimesAbstract {
    @Override
    public boolean judgeMaxTimes(Object object, CheckRequestTimes checkRequestTimes, RedisService redisService) {
        Object[] objects= (Object[])object;
        LoginBody loginBody = JSONObject.parseObject(JSONObject.toJSONString(objects[0]), LoginBody.class);
        String phone = Constants.RECRUIT_CODE_TIMES_KEY + loginBody.getUserName() + Constants.NUM;
        //本地只有一个服务器,拼接一个ip的key;如果是分布式这种方式就不太可取了根据需求来吧
        StringBuilder ip = new StringBuilder();
        ip.append(Constants.RECRUIT_CODE_TIMES_KEY).append(LocalHostUtil.getLocalIp()).append(Constants.DELIVERY).append(Constants.NUM);
        //判断本地系统的最大请求方式和用户的请求次数
        if (StringUtils.isNotEmpty(ip) && StringUtils.isNotEmpty(phone)) {
            return redisService.judgeMaxRequestTimes(ip.toString(), checkRequestTimes.maxSystermTimes()) && redisService.judgeMaxRequestTimes(phone, checkRequestTimes.maxTimes());
        }
        return false;
    }
}

RedisService判断请求方法

这里实现了一简单redis计数器自己随手写的也不知道对不对;rediscache封装的redis一些操作

/**
 * 判断最大请求次数
 *
 * @param key 缓存对象key键
 * @param max 最大请求次数
 * @return
 */
@Override
public Boolean judgeMaxRequestTimes(String key, String max) {
    //获取key值,值为null插入值
    //不为null进行,判断是否到最大值,更新数值
    String value = redisCache.getCacheObject(key);
    if (StringUtils.isEmpty(value)) {
        //key存在的话不对齐进行操作,存在的话就他设置值
        redisCache.setIfAbsent(key, RecruitNumberConstants.NUMBER_1.toString(), RecruitNumberConstants.NUMBER_24, TimeUnit.HOURS);
        return true;
    }
    //最大次数 <= 当前访问次数
    if (Integer.valueOf(max).compareTo(Integer.valueOf(value)) <= RecruitNumberConstants.NUMBER_0) {
        return false;
    }
    //这里获取的是当前key的过期时间
    //(因为这边更新值的话,更新要不得设置过期时间要不不设置更新那ttl就变成了永久的了
    //两种方案都不合理那就只能获取他当前的剩余时间去更新了)
    Long expire = redisCache.getExpire(key);
    //key存在的话对其进行更新,不存在不对其进行操作
    return redisCache.setIfPresent(key, String.valueOf(Integer.parseInt(value) + RecruitNumberConstants.NUMBER_1), expire, TimeUnit.SECONDS);
}

如何改进

个人感觉这应该是不支持并发的,关于计数的操作可以用原子类去操作;我感觉我写的这玩意分布式估计也支持不了,有时间自己搭个环境再验证吧,懒得搞了。

以上就是本文的全部内容了,能力有限,理性对待

如果感觉还不错的话,欢迎点赞和关注🦋

分享经验,贴近项目,crud永不为奴!!!

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: 要实现验证码登录,需要以下步骤: 1. 用户输入手机号码,点击发送验证码按钮,后台服务向该手机号码发送验证。 2. 用户输入收到的验证码,点击登录按钮。 3. 后台服务验证手机号码和验证码是否匹配,如果匹配则登录成功,否则登录失败。 以下是使用 Java 实现验证码登录的示例代码: ``` // 生成随机的验证码 String code = String.valueOf((int)((Math.random()*9+1)*100000)); // 将验证码存储到缓存中,这里使用了 Redis 缓存,可以根据自己的情况选择合适的缓存方式 redisTemplate.opsForValue().set("sms_" + phone, code, 5, TimeUnit.MINUTES); // 发送验证码 sendSms(phone, code); ``` ``` // 根据手机号码和验证码判断是否可以登录 String cacheCode = redisTemplate.opsForValue().get("sms_" + phone); if (!code.equals(cacheCode)) { // 验证码不正确 return "验证码不正确,请重新输入"; } // 验证码正确,进行登录操作 User user = userService.loginByPhone(phone); if (user == null) { // 用户不存在 return "用户不存在,请先注册"; } // 将用户息存储到 session 中 session.setAttribute("user", user); return "登录成功"; ``` 需要注意的是,发送验证码和验证验证码是否正确的具体实现方式需要根据所使用的服务提供商进行调整。 ### 回答2: 要实现验证码登录功能,可以使用Java编程语言结合第三方服务提供商的API来实现。 首先,我们需要调用服务提供商的API接口来发送验证。根据服务商的不同,发送的方式也有所差异,但一般来说,我们需要提供手机号码、内容和发送者等息,并通过请求发送。 接下来,用户输入手机号码和接收到的验证码后,我们需要验证验证码的有效性。可以通过与服务提供商的接口进行对比来验证验证码是否有效。一般来说,我们会将接收到的手机号码和验证码一同传递给服务商的API,并获取返回结果。如果验证码有效,我们可以继续进行登录操作;如果验证码无效,可能需要重新发送验证码或提示用户验证码有误。 在登录操作时,可以使用Java中的HttpURLConnection或HttpClient等工具来发送带有手机号码和验证码的登录请求到后台服务器。后台服务器接收到登录请求后,可以再次验证手机号码和验证码,并对用户进行身份认证。如果验证成功,可以返回登录成功的响应;如果验证失败,可以返回登录失败的响应。 总结起来,实现验证码登录需要以下步骤: 1. 调用服务提供商的API发送验证; 2. 用户输入手机号码和接收到的验证码; 3. 验证验证码的有效性; 4. 发送带有手机号码和验证码的登录请求到后台服务器; 5. 后台服务器通过验证手机号码和验证码,并进行身份认证; 6. 返回登录结果给客户端。 以上就是用Java实现验证码登录的大致过程。具体实现过程中,还需要考虑异常处理、请求参数的加密与解密、接口调用频率限制等问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

孙嵓

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

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

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

打赏作者

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

抵扣说明:

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

余额充值