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_mobile
    on 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 notification
        String pin = generatePin();
        String[] parameters = new String[]{pin};
        Boolean sendResult = sendSmsNotification(countryCode, phoneNumber, 1, parameters, true);
        if (!sendResult) {
            return false;
        }
        // Save PIN to DB
        long 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 time
        long 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 seconds
        if (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 messages
        if (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;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值