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

打印 上一主题 下一主题

主题 1953|帖子 1953|积分 5859

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
条件

用户手机号发送验证码, 需要考虑以下几点



  • 每天发送的频率(同一个手机号每天发送N条,比如限制每天发送15条)
  • 发送短信的时间隔断 (比如60秒以后才能继续发送下一条)
  • 验证码的逾期时间(比如: 5分钟)
  • 验证码最大验证次数(触发验证接口, 输入的验证码在多次失败验证下,能触发频频验证接口提示验证失败)
  • 验证码验证后,颁发一个验证Token的时效性(这个根据业务情况进行设置Token,是一次性还是偶然效性)
验证码表计划

  1. create table t_sms_code
  2. (
  3.     id             bigint       not null comment '主键'
  4.         primary key,
  5.     country_code   int          not null comment '国家代码',
  6.     mobile         varchar(50)  not null comment '手机号',
  7.     type           int          null comment '短信类型, 1 用户模块',
  8.     code           varchar(6)   null comment '验证码',
  9.     token          varchar(400) null comment '验证码token',
  10.     day_send_times varchar(400) null comment '当日发送时间列表',
  11.     send_count     int          null comment '发送次数',
  12.     send_time      bigint       null comment '发送时间',
  13.     verify_count   int          null comment '验证次数',
  14.     verify_time    bigint       null comment '验证时间'
  15. )
  16.     comment '短信验证码';
  17. create index country_code_mobile
  18.     on t_sms_code (country_code, mobile);
复制代码
要点参数设置

  1. public class SmsProperties {
  2.     /**
  3.      * 短信发送次数限制
  4.      */
  5.     private Integer maxSmsCountInDay = 10;
  6.     private Long smsSendInterval = 60 * 1000L;
  7.     /**
  8.      * 验证码过期时间
  9.      */
  10.     private Long smsCodeExpireTime = 5 * 60 * 1000L;
  11.     /**
  12.      * 验证码验证次数限制
  13.      */
  14.     private Integer smsMaxVerifyCount = 3;
  15.     /**
  16.      * 验证码token失效时间, 1 days
  17.      */
  18.     public Long smsTokenInvalidTime = 24 * 60 * 60 * 1000L;
  19.         /**
  20.         * 更加业务需要是否使用jwt-Token, 比如可以使用随机字符串作为Token
  21.         * RandomStringUtils.random(64, 0, 0, true, true, null, new SecureRandom()).toUpperCase()
  22.         */
  23.     public String secretKey = "user@jwt@miyao";
  24. }
复制代码
配合Mybatis, 编写相关API

生成验证码

  1. public Boolean generatePinAndSendSms(Integer countryCode, String phoneNumber) throws SmsException{
  2.         SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);
  3.         List<Long> daySmsRecordTimes = Lists.newArrayList();
  4.         if (Objects.nonNull(sms)) {
  5.             daySmsRecordTimes = getDaySmsRecordTimes(sms);
  6.             if (!daySmsRecordTimes.isEmpty() && daySmsRecordTimes.size() >= smsProperties.getMaxSmsCountInDay()) {
  7.                 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.",
  8.                         phoneNumber);
  9.                 throw new SmsException(SmsException.ExceptionType.TOO_MANY);
  10.             }
  11.             if (sms.getType() != null && sms.getType() == 1 && sms.getSendCount() != null && sms.getSendTime() != null) {
  12.                 checkSendingInterval(sms.getSendCount(), sms.getSendTime(), phoneNumber);
  13.             }
  14.         }
  15.         // Generate and send the notification
  16.         String pin = generatePin();
  17.         String[] parameters = new String[]{pin};
  18.         Boolean sendResult = sendSmsNotification(countryCode, phoneNumber, 1, parameters, true);
  19.         if (!sendResult) {
  20.             return false;
  21.         }
  22.         // Save PIN to DB
  23.         long timeMillis = System.currentTimeMillis();
  24.         daySmsRecordTimes.add(timeMillis);
  25.         Integer currentSendCount = Optional.ofNullable(sms).filter(s -> s.getSendCount() != null).map(s -> s.getSendCount() + 1).orElse(1);
  26.         SmsCode smsCode = new SmsCode()
  27.                 .setCountryCode(countryCode)
  28.                 .setMobile(phoneNumber)
  29.                 .setCode(pin)
  30.                 .setType(1)
  31.                 .setToken(null)
  32.                 .setDaySendTimes(JSONObject.toJSONString(daySmsRecordTimes))
  33.                 .setSendTime(timeMillis)
  34.                 .setSendCount(currentSendCount)
  35.                 .setVerifyTime(null)
  36.                 .setVerifyCount(null);
  37.         if (sms == null) {
  38.             smsCode.setId(IdWorker.getId());
  39.             this.insert(smsCode);
  40.         } else {
  41.             smsCode.setId(sms.getId());
  42.             this.update(smsCode);
  43.         }
  44.         return true;
  45.     }
  46.    public SmsCode getSmsCodeByPhoneNumber(Integer countryCode, String phoneNumber) {
  47.         return this.selectLimitOne(Query.of(new SmsCode().setMobile(phoneNumber)
  48.                         .setType(1).setCountryCode(countryCode)).wrapper());
  49.     }
  50.     private List<Long> getDaySmsRecordTimes(SmsCode smsCode) {
  51.         if (smsCode.getDaySendTimes() == null) {
  52.             return Lists.newArrayList();
  53.         }
  54.         List<Long> daySendTimes = JSONObject.parseArray(smsCode.getDaySendTimes(), Long.class);
  55.         // day start time
  56.         long dayStartTime = DateUtil.beginOfDay(new Date()).getTime();
  57.         List<Long> sendTimes = daySendTimes.stream().filter(s -> s > dayStartTime).sorted().collect(Collectors.toList());
  58.         return sendTimes;
  59.     }
  60.     private void checkSendingInterval(Integer sendCount, Long sendTime, String phoneNumber) throws SmsException {
  61.         long currentTime = System.currentTimeMillis();
  62.         Long sendInterval = smsProperties.getSmsSendInterval() != null ? smsProperties.getSmsSendInterval() : 60 * 1000L;
  63.         // The SMS pin code can be sent only once within 50 seconds
  64.         if (currentTime < sendInterval) {
  65.             log.warn("PIN code to phone number '{}' was not sent because the requests are too frequent. Please try again after {}.",
  66.                     phoneNumber, sendTime + sendInterval);
  67.             throw new SmsException(SmsException.ExceptionType.TOO_MANY);
  68.         }
  69.         // Wait five minutes after sending three messages
  70.         if (sendCount % 3 == 0) {
  71.             if (currentTime < sendTime + 5 * 60 * 1000L) {
  72.                 log.warn("PIN code to phone number '{}' was not sent because of {} sends have blocked sending until {}",
  73.                         phoneNumber, sendCount, sendTime + 5 * 60 * 1000L);
  74.                 throw new SmsException(SmsException.ExceptionType.TOO_MANY);
  75.             }
  76.         }
  77.     }
  78.     private String generatePin() {
  79.         int min = 100000;
  80.         int max = 1000000;
  81.         int randomNum = rand.nextInt((max - min)) + min;
  82.         return String.valueOf(randomNum);
  83.     }
复制代码
校验验证码

  1. public SmsVerifyResult checkPhoneAndPin(Integer countryCode, String phoneNumber, String pin, String ipAddress) throws SmsException {
  2.         if (StringUtils.isBlank(pin)) {
  3.             log.warn("PIN verification for phone number '{}' was rejected because PIN is null or whitespace", phoneNumber);
  4.             throw new SmsException(SmsException.ExceptionType.NOT_FOUND);
  5.         }
  6.         SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);
  7.         if (sms == null) {
  8.             log.info("PIN verification for phone number '{}' was rejected because verification was not found", phoneNumber);
  9.             throw new SmsException(SmsException.ExceptionType.NOT_FOUND);
  10.         }
  11.         if (StringUtils.isBlank(sms.getCode())) {
  12.             log.warn("PIN verification for phone number '{}' was rejected because pin was not found", phoneNumber);
  13.             throw new SmsException(SmsException.ExceptionType.NOT_FOUND);
  14.         }
  15.         if (StringUtils.isNotBlank(sms.getToken())) {
  16.             log.warn("PIN verification for phone number '{}' was rejected because it was already verified", phoneNumber);
  17.             throw new SmsException(SmsException.ExceptionType.CODE_VALIDATED);
  18.         }
  19.         long timeMillis = System.currentTimeMillis();
  20.         if (sms.getSendTime() != null && timeMillis > (sms.getSendTime() + smsProperties.getSmsCodeExpireTime())) {
  21.             log.info("PIN verification for phone number '{}' was rejected because it occurred too long after code was sent", phoneNumber);
  22.             throw new SmsException(SmsException.ExceptionType.CODE_EXPIRED);
  23.         }
  24.         Integer currentVerifyCount = sms.getVerifyCount() != null ? sms.getVerifyCount() + 1 : 1;
  25.         SmsCode smsCode = new SmsCode()
  26.                 .setId(sms.getId())
  27.                 .setCountryCode(sms.getCountryCode())
  28.                 .setMobile(sms.getMobile())
  29.                 .setCode(sms.getCode())
  30.                 .setType(sms.getType())
  31.                 .setToken(sms.getToken())
  32.                 .setDaySendTimes(sms.getDaySendTimes())
  33.                 .setSendTime(sms.getSendTime())
  34.                 .setSendCount(sms.getSendCount())
  35.                 .setVerifyCount(currentVerifyCount)
  36.                 .setVerifyTime(timeMillis);
  37.         if (!pin.equals(sms.getCode())) {
  38.             updateCurrentVerifyCount(currentVerifyCount, smsCode);
  39.             if(currentVerifyCount >= smsProperties.getSmsMaxVerifyCount()){
  40.                 log.warn("PIN verification for phone number '{}' rejected and {} failed attempts have set pin to invalid",
  41.                         phoneNumber, currentVerifyCount);
  42.                 throw new SmsException(SmsException.ExceptionType.CODE_VALIDATE_TOO_MANY);
  43.             }
  44.             throw new SmsException(SmsException.ExceptionType.CODE_INVALID);
  45.         }
  46.         SmsVerifyToken smsVerifyToken = new SmsVerifyToken()
  47.                 .setCountryCode(countryCode)
  48.                 .setPhoneNumber(phoneNumber)
  49.                 .setIp(ipAddress);
  50.                 // 此处根据自己的业务情况,使用一次性字符串Token还是什么有意义的token?
  51.         String token = JwtUtils.createJwt(UUID.randomUUID().toString(), JsonUtils.toJSONString(smsVerifyToken), smsProperties.getSecretKey(), smsProperties.getSmsTokenInvalidTime());
  52.         smsCode.setToken(token)
  53.                 .setCode(null)
  54.                 .setSendCount(null)
  55.                 .setSendTime(null);
  56.         this.update(smsCode);
  57.         SmsVerifyResult smsVerifyResult = new SmsVerifyResult();
  58.         smsVerifyResult.setVerifyToken(smsCode.getToken());
  59.         // 如果有其他信息,根据自己业务设置, 或者直接返回,token这个字符串
  60.         return smsVerifyResult;
  61.     }
复制代码
在进行业务流程时,校验验证码Token信息

  1. public boolean checkPhoneAndToken(Integer countryCode, String phoneNumber, String token) {
  2.         if (StringUtils.isBlank(token)) {
  3.             log.warn("Token verification for phone number '{}' was rejected because of an invalid token", phoneNumber);
  4.             return false;
  5.         }
  6.         SmsCode sms = getSmsCodeByPhoneNumber(countryCode, phoneNumber);
  7.         if (sms == null) {
  8.             log.warn("Token verification for phone number '{}' was rejected because verification was not found", phoneNumber);
  9.             return false;
  10.         }
  11.         if (!token.equals(sms.getToken())) {
  12.             log.warn("Token verification for phone number '{}' was rejected because the token did not match with the saved token", phoneNumber);
  13.             return false;
  14.         }
  15.         try {
  16.             Claims claims = JwtUtils.parseJwt(token, smsProperties.getSecretKey());
  17.             String subject = claims.getSubject();
  18.             SmsVerifyToken smsVerifyToken = JSONObject.parseObject(subject, SmsVerifyToken.class);
  19.             if (smsVerifyToken.getPhoneNumber() == null || !phoneNumber.equals(smsVerifyToken.getPhoneNumber())
  20.                 || !countryCode.equals(smsVerifyToken.getCountryCode())) {
  21.                 log.warn("Token verification for phone number '{}' was rejected because the token did not match with the saved token", phoneNumber);
  22.                 return false;
  23.             }
  24.             Date expiration = claims.getExpiration();
  25.             if (expiration.before(new Date())) {
  26.                 log.warn("Token expired");
  27.                 return false;
  28.             }
  29.         } catch (Exception e) {
  30.             return false;
  31.         }
  32. //        long timeMillis = System.currentTimeMillis();
  33. //        if (sms.getVerifyTime() == null || timeMillis > sms.getVerifyTime() + smsProperties.getSmsTokenInvalidTime()) {
  34. //            log.warn("Token verification for phone number '{}' was rejected because the token was verified too long ago", phoneNumber);
  35. //            return false;
  36. //        }
  37.         return true;
  38.     }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

知者何南

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表