大家好,我是孙嵓,短信验证码相信大家都不陌生吗,但是短信验证码怎么生成的你真的了解吗,本文揭示本人项目中对短信验证码的。
项目需求
用户注册/忘记密码添加短信验证码
需求来由
登录注册页面需要确保用户同一个手机号只关联一个账号确保非人为操作,避免系统用户信息紊乱增加系统安全性
代码实现
同事提供了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永不为奴!!!