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

