当前位置: 首页 > news >正文

MySQL手机号发送验证码设计与应用

前提

用户手机号发送验证码, 需要考虑以下几点
  • 每天发送的频率(同一个手机号每天发送N条,比如限制每天发送15条)
  • 发送短信的时间间隔 (比如60秒以后才能继续发送下一条)
  • 验证码的过期时间(比如: 5分钟)
  • 验证码最大验证次数(触发验证接口, 输入的验证码在多次失败验证下,能触发几次验证接口提示验证失败)
  • 验证码验证后,颁发一个验证Token的时效性(这个根据业务情况进行设置Token,是一次性还是有时效性)
验证码表设计
create table t_sms_code
(id             bigint       not null comment '主键'primary key,country_code   int          not null comment '国家代码',mobile         varchar(50)  not null comment '手机号',type           int          null comment '短信类型, 1 用户模块',code           varchar(6)   null comment '验证码',token          varchar(400) null comment '验证码token',day_send_times varchar(400) null comment '当日发送时间列表',send_count     int          null comment '发送次数',send_time      bigint       null comment '发送时间',verify_count   int          null comment '验证次数',verify_time    bigint       null comment '验证时间'
)comment '短信验证码';create index country_code_mobileon t_sms_code (country_code, mobile);
要点参数配置
public class SmsProperties {/*** 短信发送次数限制*/private Integer maxSmsCountInDay = 10;private Long smsSendInterval = 60 * 1000L;/*** 验证码过期时间*/private Long smsCodeExpireTime = 5 * 60 * 1000L;/*** 验证码验证次数限制*/private Integer smsMaxVerifyCount = 3;/*** 验证码token失效时间, 1 days*/public Long smsTokenInvalidTime = 24 * 60 * 60 * 1000L;/*** 更加业务需要是否使用jwt-Token, 比如可以使用随机字符串作为Token* RandomStringUtils.random(64, 0, 0, true, true, null, new SecureRandom()).toUpperCase()*/public String secretKey = "user@jwt@miyao";
}
配合Mybatis, 编写相关API
生成验证码
public Boolean generatePinAndSendSms(Integer countryCode, String phoneNumber) throws SmsException{SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);List<Long> daySmsRecordTimes = Lists.newArrayList();if (Objects.nonNull(sms)) {daySmsRecordTimes = getDaySmsRecordTimes(sms);if (!daySmsRecordTimes.isEmpty() && daySmsRecordTimes.size() >= smsProperties.getMaxSmsCountInDay()) {log.warn("PIN code to phone number '{}' was not sent because the maximum number of sends allowed by the SMS operator within 24 hours has been exceeded.",phoneNumber);throw new SmsException(SmsException.ExceptionType.TOO_MANY);}if (sms.getType() != null && sms.getType() == 1 && sms.getSendCount() != null && sms.getSendTime() != null) {checkSendingInterval(sms.getSendCount(), sms.getSendTime(), phoneNumber);}}// Generate and send the notificationString pin = generatePin();String[] parameters = new String[]{pin};Boolean sendResult = sendSmsNotification(countryCode, phoneNumber, 1, parameters, true);if (!sendResult) {return false;}// Save PIN to DBlong timeMillis = System.currentTimeMillis();daySmsRecordTimes.add(timeMillis);Integer currentSendCount = Optional.ofNullable(sms).filter(s -> s.getSendCount() != null).map(s -> s.getSendCount() + 1).orElse(1);SmsCode smsCode = new SmsCode().setCountryCode(countryCode).setMobile(phoneNumber).setCode(pin).setType(1).setToken(null).setDaySendTimes(JSONObject.toJSONString(daySmsRecordTimes)).setSendTime(timeMillis).setSendCount(currentSendCount).setVerifyTime(null).setVerifyCount(null);if (sms == null) {smsCode.setId(IdWorker.getId());this.insert(smsCode);} else {smsCode.setId(sms.getId());this.update(smsCode);}return true;}public SmsCode getSmsCodeByPhoneNumber(Integer countryCode, String phoneNumber) {return this.selectLimitOne(Query.of(new SmsCode().setMobile(phoneNumber).setType(1).setCountryCode(countryCode)).wrapper());}private List<Long> getDaySmsRecordTimes(SmsCode smsCode) {if (smsCode.getDaySendTimes() == null) {return Lists.newArrayList();}List<Long> daySendTimes = JSONObject.parseArray(smsCode.getDaySendTimes(), Long.class);// day start timelong dayStartTime = DateUtil.beginOfDay(new Date()).getTime();List<Long> sendTimes = daySendTimes.stream().filter(s -> s > dayStartTime).sorted().collect(Collectors.toList());return sendTimes;}private void checkSendingInterval(Integer sendCount, Long sendTime, String phoneNumber) throws SmsException {long currentTime = System.currentTimeMillis();Long sendInterval = smsProperties.getSmsSendInterval() != null ? smsProperties.getSmsSendInterval() : 60 * 1000L;// The SMS pin code can be sent only once within 50 secondsif (currentTime < sendInterval) {log.warn("PIN code to phone number '{}' was not sent because the requests are too frequent. Please try again after {}.",phoneNumber, sendTime + sendInterval);throw new SmsException(SmsException.ExceptionType.TOO_MANY);}// Wait five minutes after sending three messagesif (sendCount % 3 == 0) {if (currentTime < sendTime + 5 * 60 * 1000L) {log.warn("PIN code to phone number '{}' was not sent because of {} sends have blocked sending until {}",phoneNumber, sendCount, sendTime + 5 * 60 * 1000L);throw new SmsException(SmsException.ExceptionType.TOO_MANY);}}}private String generatePin() {int min = 100000;int max = 1000000;int randomNum = rand.nextInt((max - min)) + min;return String.valueOf(randomNum);}
校验验证码
public SmsVerifyResult checkPhoneAndPin(Integer countryCode, String phoneNumber, String pin, String ipAddress) throws SmsException {if (StringUtils.isBlank(pin)) {log.warn("PIN verification for phone number '{}' was rejected because PIN is null or whitespace", phoneNumber);throw new SmsException(SmsException.ExceptionType.NOT_FOUND);}SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);if (sms == null) {log.info("PIN verification for phone number '{}' was rejected because verification was not found", phoneNumber);throw new SmsException(SmsException.ExceptionType.NOT_FOUND);}if (StringUtils.isBlank(sms.getCode())) {log.warn("PIN verification for phone number '{}' was rejected because pin was not found", phoneNumber);throw new SmsException(SmsException.ExceptionType.NOT_FOUND);}if (StringUtils.isNotBlank(sms.getToken())) {log.warn("PIN verification for phone number '{}' was rejected because it was already verified", phoneNumber);throw new SmsException(SmsException.ExceptionType.CODE_VALIDATED);}long timeMillis = System.currentTimeMillis();if (sms.getSendTime() != null && timeMillis > (sms.getSendTime() + smsProperties.getSmsCodeExpireTime())) {log.info("PIN verification for phone number '{}' was rejected because it occurred too long after code was sent", phoneNumber);throw new SmsException(SmsException.ExceptionType.CODE_EXPIRED);}Integer currentVerifyCount = sms.getVerifyCount() != null ? sms.getVerifyCount() + 1 : 1;SmsCode smsCode = new SmsCode().setId(sms.getId()).setCountryCode(sms.getCountryCode()).setMobile(sms.getMobile()).setCode(sms.getCode()).setType(sms.getType()).setToken(sms.getToken()).setDaySendTimes(sms.getDaySendTimes()).setSendTime(sms.getSendTime()).setSendCount(sms.getSendCount()).setVerifyCount(currentVerifyCount).setVerifyTime(timeMillis);if (!pin.equals(sms.getCode())) {updateCurrentVerifyCount(currentVerifyCount, smsCode);if(currentVerifyCount >= smsProperties.getSmsMaxVerifyCount()){log.warn("PIN verification for phone number '{}' rejected and {} failed attempts have set pin to invalid",phoneNumber, currentVerifyCount);throw new SmsException(SmsException.ExceptionType.CODE_VALIDATE_TOO_MANY);}throw new SmsException(SmsException.ExceptionType.CODE_INVALID);}SmsVerifyToken smsVerifyToken = new SmsVerifyToken().setCountryCode(countryCode).setPhoneNumber(phoneNumber).setIp(ipAddress);// 此处根据自己的业务情况,使用一次性字符串Token还是什么有意义的token?String token = JwtUtils.createJwt(UUID.randomUUID().toString(), JsonUtils.toJSONString(smsVerifyToken), smsProperties.getSecretKey(), smsProperties.getSmsTokenInvalidTime());smsCode.setToken(token).setCode(null).setSendCount(null).setSendTime(null);this.update(smsCode);SmsVerifyResult smsVerifyResult = new SmsVerifyResult();smsVerifyResult.setVerifyToken(smsCode.getToken());// 如果有其他信息,根据自己业务设置, 或者直接返回,token这个字符串return smsVerifyResult;}
在进行业务流程时,校验验证码Token信息
 public boolean checkPhoneAndToken(Integer countryCode, String phoneNumber, String token) {if (StringUtils.isBlank(token)) {log.warn("Token verification for phone number '{}' was rejected because of an invalid token", phoneNumber);return false;}SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);if (sms == null) {log.warn("Token verification for phone number '{}' was rejected because verification was not found", phoneNumber);return false;}if (!token.equals(sms.getToken())) {log.warn("Token verification for phone number '{}' was rejected because the token did not match with the saved token", phoneNumber);return false;}try {Claims claims = JwtUtils.parseJwt(token, smsProperties.getSecretKey());String subject = claims.getSubject();SmsVerifyToken smsVerifyToken = JSONObject.parseObject(subject, SmsVerifyToken.class);if (smsVerifyToken.getPhoneNumber() == null || !phoneNumber.equals(smsVerifyToken.getPhoneNumber())|| !countryCode.equals(smsVerifyToken.getCountryCode())) {log.warn("Token verification for phone number '{}' was rejected because the token did not match with the saved token", phoneNumber);return false;}Date expiration = claims.getExpiration();if (expiration.before(new Date())) {log.warn("Token expired");return false;}} catch (Exception e) {return false;}
//        long timeMillis = System.currentTimeMillis();
//        if (sms.getVerifyTime() == null || timeMillis > sms.getVerifyTime() + smsProperties.getSmsTokenInvalidTime()) {
//            log.warn("Token verification for phone number '{}' was rejected because the token was verified too long ago", phoneNumber);
//            return false;
//        }return true;}

相关文章:

  • Pandas数据可视化宝典:解锁图形绘制与样式自定义的奥秘
  • vscode使用记录
  • PXE、Kickstart和cobbler
  • 数据结构(C语言版)-第二章线性表
  • Windows 虚拟机服务器项目部署
  • Spring MVC 全注解开发
  • Go语言--广播式并发聊天服务器
  • TCP重传、滑动窗口、流量控制、拥塞控制机制
  • 【堆 优先队列 第k大】2551. 将珠子放入背包中
  • Flask启动5000端口后关不掉了?
  • 云原生(Cloud native)
  • AV1 编码标准中帧内预测技术概述
  • 黑马头条-环境搭建、SpringCloud
  • 云盘挂载 开机自动模拟 cmd- alist server
  • 笔记 2 :linux 0.11 中的重要的全局变量 (a)
  • 分享一款快速APP功能测试工具
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • Android Studio:GIT提交项目到远程仓库
  • Android系统模拟器绘制实现概述
  • go语言学习初探(一)
  • JavaScript标准库系列——Math对象和Date对象(二)
  • JavaScript的使用你知道几种?(上)
  • React中的“虫洞”——Context
  • 大整数乘法-表格法
  • 扑朔迷离的属性和特性【彻底弄清】
  • 吐槽Javascript系列二:数组中的splice和slice方法
  • 我看到的前端
  • 一起来学SpringBoot | 第三篇:SpringBoot日志配置
  • 《天龙八部3D》Unity技术方案揭秘
  • Redis4.x新特性 -- 萌萌的MEMORY DOCTOR
  • 阿里云重庆大学大数据训练营落地分享
  • #职场发展#其他
  • (02)vite环境变量配置
  • (2024)docker-compose实战 (9)部署多项目环境(LAMP+react+vue+redis+mysql+nginx)
  • (a /b)*c的值
  • (poj1.3.2)1791(构造法模拟)
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (力扣)循环队列的实现与详解(C语言)
  • (强烈推荐)移动端音视频从零到上手(下)
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (十)Flink Table API 和 SQL 基本概念
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (转)使用VMware vSphere标准交换机设置网络连接
  • .net CHARTING图表控件下载地址
  • .NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库
  • .NET Core引入性能分析引导优化
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .Net(C#)自定义WinForm控件之小结篇
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • .skip() 和 .only() 的使用
  • .xml 下拉列表_RecyclerView嵌套recyclerview实现二级下拉列表,包含自定义IOS对话框...
  • @AliasFor注解
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798