Java實現(xiàn)短信驗證碼的示例代碼
短信驗證碼相信大家都不陌生嗎,但是短信驗證碼怎么生成的你真的了解嗎,本文揭示本人項目中對短信驗證碼的。
項目需求
用戶注冊/忘記密碼添加短信驗證碼
需求來由
登錄注冊頁面需要確保用戶同一個手機號只關(guān)聯(lián)一個賬號確保非人為操作,避免系統(tǒng)用戶信息紊亂增加系統(tǒng)安全性
代碼實現(xiàn)
同事提供了WebService接口,很好,之前沒調(diào)過,又增加了困難。
這邊用的阿里云的短信服務(wù),廢話少說上圖,呸,上代碼—
發(fā)送驗證碼方法
public AjaxResult sendVerificationCode(LoginBody loginBody) { //拼裝redis的key String redisCodeKey = Constants.RECRUIT_CODE_KEY + loginBody.getUserName(); //通過判斷過期時間檢驗是否發(fā)送過驗證碼如果發(fā)送直接return if (redisCache.getExpire(redisCodeKey) >= 0) { return AjaxResult.error(TipsConstants.YZM_SEND_ALREADY); } //生成隨機6位驗證碼 String redisCodeValue = VerifyCodeUtils.generateSmsCode(); //驗證碼類型這是根據(jù)同事給的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格式的參數(shù) JSONObject jsonObject = new JSONObject(); jsonObject.put(WebServiceConstants.CODE, redisCodeValue); Map<String, String> resultMap = SMSUtils.sendMessage(loginBody.getUserName(),templateCode,jsonObject); //判斷webservice接口返回的結(jié)果 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設(shè)置過期時間,這里設(shè)置了60s,根據(jù)需求來 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,創(chuàng)建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(); }
前端代碼
這里只粘貼了發(fā)送驗證碼改變按鈕的方法
sendCode(type) { this.$refs.registerForm.validateField('phone',(phoneError)=> { if(!phoneError){ this.registerForm.verificationCodeType = type //短信驗證碼最大請求次數(shù)校驗 getSmsCode(this.registerForm).then(response => { if (response.code !== 200) { this.requestMax = true } else { this.msgSuccess('發(fā)送成功,請注意查收短信') this.requestMax = false } //發(fā)送驗證碼按鈕修改 if (!this.requestMax) { let time = 60 this.buttonText = '已發(fā)送' 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如何調(diào)用?
一開始導(dǎo)了很多關(guān)于webservice的相關(guān)依賴,結(jié)果掉不通沒辦法只能用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.不能讓用戶無限制的請求發(fā)送驗證碼
據(jù)說短信平臺有驗證邏輯,為了安全還是給系統(tǒng)封了一層;這里通過注解,aop配合redis計數(shù)器進(jìn)行最大請求次數(shù)驗證。
代碼如下
注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckRequestTimes { /** * 最大請求次數(shù) */ String maxTimes() default "10"; /** * 整個系統(tǒng)最大請求次數(shù) */ String maxSystermTimes() default "1000"; /** * 請求類型 */ RequestEnums reqType() default RequestEnums.COMMON; /** * 請求次數(shù)上限錯誤信息提示 */ String errorMsg() default TipsConstants.REQUEST_TIMES_MAX }
Aspect
這部分代碼我個人認(rèn)為設(shè)計比較巧妙,可供讀者思考,多利用設(shè)計模式思想去開發(fā)代碼,讓代碼更優(yōu)雅、更健壯、更可用,crud也有編出自己的骨氣?。。?本實例涵蓋了單例,模板方法)
@Aspect @Component @Order(2) public class CheckRequestAspect { ? ? @Autowired ? ? RedisService redisService; ? ? @Autowired ? ? TokenService tokenService; ? ? private static Logger logger = LoggerFactory.getLogger(CheckRequestAspect.class); ? ? //防止并發(fā),添加關(guān)鍵字實現(xiàn)共享 ? ? private volatile ConcurrentHashMap<RequestEnums, RequestTimesAbstract> reqTimesProcessMap; ? ?? ? ? @PostConstruct ? ? public void initExcelProcessorFactory() { ? ? ? ? //dcl 雙重檢查鎖,也可進(jìn)行懶散加載。因為現(xiàn)在基于spring容器單例,此鎖可適當(dāng)調(diào)整 ? ? ? ? if (MapUtil.isNotEmpty(reqTimesProcessMap)) { ? ? ? ? ? ? return; ? ? ? ? } ? ? ? ? //眼熟不這叫懶漢式單例 ? ? ? ? synchronized (this) { ? ? ? ? ? ? if (ObjectUtil.isNull(reqTimesProcessMap)) { ? ? ? ? ? ? ? ? reqTimesProcessMap = new ConcurrentHashMap(8); ? ? ? ? ? ? } ? ? ? ? ? ? //這里其實可以采用工廠方法去改造,由于業(yè)務(wù)沒有太多類型所以就不設(shè)計工廠了 ? ? ? ? ? ? reqTimesProcessMap.put(RequestEnums.COMMON, new UserCommReqTimes()); ? ? ? ? ? ? reqTimesProcessMap.put(RequestEnums.SMS, new SMSCodeReqTimes()); ? ? ? ? } ? ? } ? ? /** ? ? ?* 切入點 ? ? ?*/ ? ? @Pointcut("@annotation(com.fuwai.hr.common.annotation.CheckRequestTimes)") ? ? public void checkPoint() { ? ? } ? ? /** ? ? ?* 環(huán)繞獲取請求參數(shù) ? ? ?* ? ? ?* @param proceedingJoinPoint ? ? ?* @return ? ? ?*/ ? ? @Around("checkPoint()") ? ? public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) { ? ? ? ? //獲取方法上的注解 ? ? ? ? CheckRequestTimes checkRequestTimes = getAnnotation(proceedingJoinPoint); ? ? ? ? Object[] args = proceedingJoinPoint.getArgs(); ? ? ? ? //判斷是否到達(dá)最大請求次數(shù),這里為了應(yīng)對不同請求類型的處理方式寫了一個抽象類, ? ? ? ? //便于擴展維護(hù),沿用了了模板方法設(shè)計模式的思想 ? ? ? ? if(!reqTimesProcessMap.get(checkRequestTimes.reqType()).judgeMaxTimes(args, checkRequestTimes, redisService)){ ? ? ? ? ? ? return AjaxResult.error(HttpStatus.REQUEST_MAX, checkRequestTimes.errorMsg()); ? ? ? ? } ? ? ? ? //執(zhí)行請求方法 ? ? ? ? Object proceed = null; ? ? ? ? try { ? ? ? ? ? ? proceed = proceedingJoinPoint.proceed(); ? ? ? ? } catch (Throwable throwable) { ? ? ? ? ? ? logger.error(throwable.getMessage(), throwable); ? ? ? ? } ? ? ? ? return proceed; ? ? } ? ? /** ? ? ?* 獲取方法上的注解以便拿到對應(yīng)的值 ? ? ?* ? ? ?* @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 { /** * 判斷是否到達(dá)請求最大次數(shù) * @param object 參數(shù) * @param checkRequestTimes 注解 * @param redisService redis服務(wù) * @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; //本地只有一個服務(wù)器,拼接一個ip的key;如果是分布式這種方式就不太可取了根據(jù)需求來吧 StringBuilder ip = new StringBuilder(); ip.append(Constants.RECRUIT_CODE_TIMES_KEY).append(LocalHostUtil.getLocalIp()).append(Constants.DELIVERY).append(Constants.NUM); //判斷本地系統(tǒng)的最大請求方式和用戶的請求次數(shù) if (StringUtils.isNotEmpty(ip) && StringUtils.isNotEmpty(phone)) { return redisService.judgeMaxRequestTimes(ip.toString(), checkRequestTimes.maxSystermTimes()) && redisService.judgeMaxRequestTimes(phone, checkRequestTimes.maxTimes()); } return false; } }
RedisService判斷請求方法
這里實現(xiàn)了一簡單redis計數(shù)器自己隨手寫的也不知道對不對;rediscache封裝的redis一些操作
/** * 判斷最大請求次數(shù) * * @param key 緩存對象key鍵 * @param max 最大請求次數(shù) * @return */ @Override public Boolean judgeMaxRequestTimes(String key, String max) { //獲取key值,值為null插入值 //不為null進(jìn)行,判斷是否到最大值,更新數(shù)值 String value = redisCache.getCacheObject(key); if (StringUtils.isEmpty(value)) { //key存在的話不對齊進(jìn)行操作,存在的話就他設(shè)置值 redisCache.setIfAbsent(key, RecruitNumberConstants.NUMBER_1.toString(), RecruitNumberConstants.NUMBER_24, TimeUnit.HOURS); return true; } //最大次數(shù) <= 當(dāng)前訪問次數(shù) if (Integer.valueOf(max).compareTo(Integer.valueOf(value)) <= RecruitNumberConstants.NUMBER_0) { return false; } //這里獲取的是當(dāng)前key的過期時間 //(因為這邊更新值的話,更新要不得設(shè)置過期時間要不不設(shè)置更新那ttl就變成了永久的了 //兩種方案都不合理那就只能獲取他當(dāng)前的剩余時間去更新了) Long expire = redisCache.getExpire(key); //key存在的話對其進(jìn)行更新,不存在不對其進(jìn)行操作 return redisCache.setIfPresent(key, String.valueOf(Integer.parseInt(value) + RecruitNumberConstants.NUMBER_1), expire, TimeUnit.SECONDS); }
如何改進(jìn)
個人感覺這應(yīng)該是不支持并發(fā)的,關(guān)于計數(shù)的操作可以用原子類去操作;我感覺我寫的這玩意分布式估計也支持不了,有時間自己搭個環(huán)境再驗證吧,懶得搞了。
到此這篇關(guān)于Java實現(xiàn)短信驗證碼的示例代碼的文章就介紹到這了,更多相關(guān)Java 短信驗證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java8新特性之再見Permgen_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了Java8新特性之再見Permgen的相關(guān)知識,非常不錯,具有參考借鑒價值,需要的的朋友參考下吧2017-06-06Mybatis批量插入index out of range錯誤的解決(較偏的錯誤)
這篇文章主要介紹了Mybatis批量插入index out of range錯誤的解決(較偏的錯誤),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12spring cloud-給Eureka Server加上安全的用戶認(rèn)證詳解
這篇文章主要介紹了spring cloud-給Eureka Server加上安全的用戶認(rèn)證詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01SpringBoot DBUnit 單元測試(小結(jié))
這篇文章主要介紹了SpringBoot DBUnit 單元測試(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09java微信公眾號開發(fā)(搭建本地測試環(huán)境)
這篇文章主要介紹了java微信公眾號開發(fā),主要內(nèi)容有測試公眾號與本地測試環(huán)境搭建,需要的朋友可以參考下2015-12-12SpringDataMongoDB多文檔事務(wù)的實現(xiàn)
mongodb4.0也出來一段時間了,這個版本最為大眾期待的特性就是支持了多文檔事務(wù)。這篇文章主要介紹了SpringDataMongoDB多文檔事務(wù)的實現(xiàn),感興趣的小伙伴們可以參考一下2018-11-11ScrollView中嵌入ListView只顯示一條的解決辦法
在ScrollView添加一個ListView會導(dǎo)致listview控件顯示不全,通常只會顯示一條,究竟是什么原因呢?下面腳本之家小編給大家介紹ScrollView中嵌入ListView只顯示一條的解決辦法,感興趣的朋友一起學(xué)習(xí)吧2016-05-05