脚本之家

电脑版
提示:原网页已由神马搜索转码, 内容由www.jb51.net提供.
您的位置:首页软件编程java→ Java 短信验证码

Java实现短信验证码的示例代码

  更新时间:2022年03月09日 09:37:07  作者:鸿鹄ing 
本文主要介绍了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 短信验证码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

    • 这篇文章主要介绍了Java8新特性之再见Permgen的相关知识,非常不错,具有参考借鉴价值,需要的的朋友参考下吧
      2017-06-06
    • 这篇文章主要介绍了Mybatis批量插入index out of range错误的解决(较偏的错误),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
      2021-12-12
    • 这篇文章主要介绍了spring cloud-给Eureka Server加上安全的用户认证详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2018-01-01
    • 今天我们来完成一个使用netty进行文件传输的任务。在实际项目中,文件传输通常采用FTP或者HTTP附件的方式,对Netty 文件上传功能感兴趣的朋友一起看看吧
      2021-07-07
    • 这篇文章介绍了Java实现字符串反转的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
      2022-04-04
    • 这篇文章主要介绍了SpringBoot DBUnit 单元测试(小结),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2018-09-09
    • 这篇文章主要介绍了java微信公众号开发,主要内容有测试公众号与本地测试环境搭建,需要的朋友可以参考下
      2015-12-12
    • mongodb4.0也出来一段时间了,这个版本最为大众期待的特性就是支持了多文档事务。这篇文章主要介绍了SpringDataMongoDB多文档事务的实现,感兴趣的小伙伴们可以参考一下
      2018-11-11
    • 这篇文章主要介绍了MybatisPlus分页排序查询字段带有下划线的坑及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
      2021-12-12
    • 在ScrollView添加一个ListView会导致listview控件显示不全,通常只会显示一条,究竟是什么原因呢?下面脚本之家小编给大家介绍ScrollView中嵌入ListView只显示一条的解决办法,感兴趣的朋友一起学习吧
      2016-05-05

    最新评论