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);
}

如何改進

個人感覺這應該是不支持並發的,關於計數的操作可以用原子類去操作;我感覺我寫的這玩意分佈式估計也支持不瞭,有時間自己搭個環境再驗證吧,懶得搞瞭。

到此這篇關於Java實現短信驗證碼的示例代碼的文章就介紹到這瞭,更多相關Java 短信驗證碼內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: