文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

短信验证码—Java实现

2023-08-17 21:37

关注

在业务需求中我们经常会用到短信验证码,比如手机号登录、绑定手机号、忘记密码、敏感操作等,都可以通过短信验证码来保证操作的安全性,于是就记录下了一次开发的过程。

image-20230429224839744

① 环境搭建

                        org.springframework.boot            spring-boot-starter-web                            org.springframework.boot            spring-boot-starter-data-redis                            org.springframework.boot            spring-boot-starter-amqp                                    com.google.code.gson            gson            2.9.0                                    org.apache.commons            commons-lang3            3.12.0                            cn.hutool            hutool-all            5.8.9                            org.springframework.boot            spring-boot-configuration-processor            true                            org.projectlombok            lombok            true                            org.springframework.boot            spring-boot-starter-test            test                                    junit            junit            4.13.2            test            

② 令牌桶算法

这里使用Redis实现令牌桶算法,令牌桶算法具体细节可参考其他博客,这里不赘述,大致就是在 一个时间段 内,存在一定数量的令牌,我们需要拿到令牌才可以继续操作。

所以实现思路大致就是:

@Componentpublic class RedisTokenBucket {    @Resource    private RedisTemplate redisTemplate;        private final long EXPIRE_TIME = 400;        public boolean tryAcquire(String phoneNum) {        // 每个手机号码一分钟内只能发送一条短信        int permitsPerMinute = 1;        // 令牌桶容量        int maxPermits = 1;        // 获取当前时间戳        long now = System.currentTimeMillis();        String key = RedisConstant.SMS_BUCKET_PREFIX + phoneNum;        // 计算令牌桶内令牌数        int tokens = Integer.parseInt(redisTemplate.opsForValue().get(key + "_tokens") == null ? "0" : redisTemplate.opsForValue().get(key + "_tokens"));        // 计算令牌桶上次填充的时间戳        long lastRefillTime = Long.parseLong(redisTemplate.opsForValue().get(key + "_last_refill_time") == null ? "0" : redisTemplate.opsForValue().get(key + "_last_refill_time"));        // 计算当前时间与上次填充时间的时间差        long timeSinceLast = now - lastRefillTime;        // 计算需要填充的令牌数        int refill = (int) (timeSinceLast / 1000 * permitsPerMinute / 60);        // 更新令牌桶内令牌数        tokens = Math.min(refill + tokens, maxPermits);        // 更新上次填充时间戳        redisTemplate.opsForValue().set(key + "_last_refill_time", String.valueOf(now),EXPIRE_TIME, TimeUnit.SECONDS);        // 如果令牌数大于等于1,则获取令牌        if (tokens >= 1) {            tokens--;            redisTemplate.opsForValue().set(key + "_tokens", String.valueOf(tokens),EXPIRE_TIME, TimeUnit.SECONDS);            // 如果获取到令牌,则返回true            return true;        }        // 如果没有获取到令牌,则返回false        return false;    }}

③ 业务代码

0.Pojo

@Data@AllArgsConstructorpublic class SmsDTO implements Serializable {    private static final long serialVersionUID = 8504215015474691352L;    String phoneNum;    String code;}

1.Controller

        @GetMapping("/smsCaptcha")    public BaseResponse smsCaptcha(@RequestParam String phoneNum){        userService.sendSmsCaptcha(phoneNum);        // 异步发送验证码,这里直接返回成功即可        return ResultUtils.success("获取短信验证码成功!");    }

2.Service

    public Boolean sendSmsCaptcha(String phoneNum) {        if (StringUtils.isEmpty(phoneNum)) {            throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号不能为空");        }        AuthPhoneNumberUtil authPhoneNumberUtil = new AuthPhoneNumberUtil();        // 手机号码格式校验        boolean checkPhoneNum = authPhoneNumberUtil.isPhoneNum(phoneNum);        if (!checkPhoneNum) {            throw new BusinessException(ErrorCode.PARAMS_ERROR, "手机号格式错误");        }        //生成随机验证码        int code = (int) ((Math.random() * 9 + 1) * 10000);        SmsDTO smsDTO = new SmsDTO(phoneNum,String.valueOf(code));        return smsUtils.sendSms(smsDTO);    }

3.发送短信工具类

@Component@Slf4jpublic class SmsUtils {    @Resource    private RedisTemplate redisTemplate;    @Resource    private RedisTokenBucket redisTokenBucket;    @Resource    private RabbitMqUtils rabbitMqUtils;    public boolean sendSms(SmsDTO smsDTO) {        // 从令牌桶中取得令牌,未取得不允许发送短信        boolean acquire = redisTokenBucket.tryAcquire(smsDTO.getPhoneNum());        if (!acquire) {            log.info("phoneNum:{},send SMS frequent", smsDTO.getPhoneNum());            return false;        }        log.info("发送短信:{}",smsDTO);        String phoneNum = smsDTO.getPhoneNum();        String code = smsDTO.getCode();        // 将手机号对应的验证码存入Redis,方便后续检验        redisTemplate.opsForValue().set(RedisConstant.SMS_CODE_PREFIX + phoneNum, String.valueOf(code), 5, TimeUnit.MINUTES);        // 利用消息队列,异步发送短信        rabbitMqUtils.sendSmsAsync(smsDTO);        return true;    }    public boolean verifyCode(String phoneNum, String code) {        String key = RedisConstant.SMS_CODE_PREFIX + phoneNum;        String checkCode = redisTemplate.opsForValue().get(key);        if (StringUtils.isNotBlank(code) && code.equals(checkCode)) {            redisTemplate.delete(key);            return true;        }        return false;    }}

4.RabbitMq初始化

创建交换机和消息队列

@Slf4j@Configurationpublic class RabbitMqConfig {        @Bean    public Queue smsQueue(){        Map arguments = new HashMap<>();        //声明死信队列和交换机消息,过期时间:1分钟        arguments.put("x-dead-letter-exchange", SMS_EXCHANGE_NAME);        arguments.put("x-dead-letter-routing-key", SMS_DELAY_EXCHANGE_ROUTING_KEY);        arguments.put("x-message-ttl", 60000);        return new Queue(SMS_QUEUE_NAME,true,false,false ,arguments);    }        @Bean    public Queue deadLetter(){        return new Queue(SMS_DELAY_QUEUE_NAME, true, false, false);    }        @Bean    public Exchange smsExchange() {        return new TopicExchange(SMS_EXCHANGE_NAME, true, false);    }        @Bean    public Binding smsBinding(){        return new Binding(SMS_QUEUE_NAME, Binding.DestinationType.QUEUE,SMS_EXCHANGE_NAME,SMS_EXCHANGE_ROUTING_KEY,null);    }        @Bean    public Binding smsDelayBinding(){        return new Binding(SMS_DELAY_QUEUE_NAME, Binding.DestinationType.QUEUE,SMS_EXCHANGE_NAME,SMS_DELAY_EXCHANGE_ROUTING_KEY,null);    }}

5.Mq短信消息生产者

@Component@Slf4jpublic class RabbitMqUtils implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {    @Resource    private RedisTemplate redisTemplate;    @Resource    private RabbitTemplate rabbitTemplate;    private String finalId = null;    private SmsDTO smsDTO = null;        public void sendSmsAsync(SmsDTO smsDTO) {        String messageId = null;        try {            // 将 headers 添加到 MessageProperties 中,并发送消息            messageId = UUID.randomUUID().toString();            HashMap messageArgs = new HashMap<>();            messageArgs.put("retryCount", 0);            //消息状态:0-未投递、1-已投递            messageArgs.put("status", 0);            messageArgs.put("smsTo", smsDTO);            //将重试次数和短信发送状态存入redis中去,并设置过期时间            redisTemplate.opsForHash().putAll(RedisConstant.SMS_MESSAGE_PREFIX + messageId, messageArgs);            redisTemplate.expire(RedisConstant.SMS_MESSAGE_PREFIX + messageId, 10, TimeUnit.MINUTES);            String finalMessageId = messageId;            finalId = messageId;            this.smsDTO = smsDTO;            // 将消息投递到MQ,并设置消息的一些参数            rabbitTemplate.convertAndSend(RabbitMqConstant.SMS_EXCHANGE_NAME, RabbitMqConstant.SMS_EXCHANGE_ROUTING_KEY, smsDTO, message -> {                MessageProperties messageProperties = message.getMessageProperties();                //生成全局唯一id                messageProperties.setMessageId(finalMessageId);                messageProperties.setContentEncoding("utf-8");                return message;            });        } catch (Exception e) {            //出现异常,删除该短信id对应的redis,并将该失败消息存入到“死信”redis中去,然后使用定时任务去扫描该key,并重新发送到mq中去            redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId);            redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, messageId, smsDTO);            throw new RuntimeException(e);        }    }        @Override    public void confirm(CorrelationData correlationData, boolean b, String s) {        // 消息发送成功,将redis中消息的状态(status)修改为1        if (b) {            redisTemplate.opsForHash().put(RedisConstant.SMS_MESSAGE_PREFIX + finalId, "status", 1);        } else {            // 发送失败,放入redis失败集合中,并删除集合数据            log.error("短信消息投送失败:{}-->{}", correlationData, s);            redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + finalId);            redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, finalId, this.smsDTO);        }    }        @Override    public void returnedMessage(ReturnedMessage returnedMessage) {        log.error("发生异常,返回消息回调:{}", returnedMessage);        // 发送失败,放入redis失败集合中,并删除集合数据        redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + finalId);        redisTemplate.opsForHash().put(RedisConstant.MQ_PRODUCER, finalId, this.smsDTO);    }    @PostConstruct    public void init() {        rabbitTemplate.setConfirmCallback(this);        rabbitTemplate.setReturnsCallback(this);    }}

6.Mq消息监听器

@Component@Slf4jpublic class SendSmsListener {    @Resource    private RedisTemplate redisTemplate;    @Resource    private SendSmsUtils sendSmsUtils;        @RabbitListener(queues = SMS_QUEUE_NAME)    public void sendSmsListener(SmsDTO smsDTO, Message message, Channel channel) throws IOException {        String messageId = message.getMessageProperties().getMessageId();        int retryCount = (int) redisTemplate.opsForHash().get(RedisConstant.SMS_MESSAGE_PREFIX + messageId, "retryCount");        if (retryCount > 3) {            //重试次数大于3,直接放到死信队列            log.error("短信消息重试超过3次:{}",  messageId);            //basicReject方法拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。            //该方法reject后,该消费者还是会消费到该条被reject的消息。            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);            redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId);            return;        }        try {            String phoneNum = smsDTO.getPhoneNum();            String code = smsDTO.getCode();            if(StringUtils.isAnyBlank(phoneNum,code)){                throw new RuntimeException("sendSmsListener参数为空");            }            // 发送消息            SendSmsResponse sendSmsResponse = sendSmsUtils.sendSmsResponse(phoneNum, code);            SendStatus[] sendStatusSet = sendSmsResponse.getSendStatusSet();            SendStatus sendStatus = sendStatusSet[0];            if(!"Ok".equals(sendStatus.getCode()) ||!"send success".equals(sendStatus.getMessage())){                throw new RuntimeException("发送验证码失败");            }            //手动确认消息            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);            log.info("短信发送成功:{}",smsDTO);            redisTemplate.delete(RedisConstant.SMS_MESSAGE_PREFIX + messageId);        } catch (Exception e) {            redisTemplate.opsForHash().put(RedisConstant.SMS_MESSAGE_PREFIX+messageId,"retryCount",retryCount+1);            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);        }    }        @RabbitListener(queues = SMS_DELAY_QUEUE_NAME)    public void smsDelayQueueListener(SmsDTO sms, Message message, Channel channel) throws IOException {        try{            log.error("监听到死信队列消息==>{}",sms);            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);        }catch (Exception e){            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);        }    }}

7.腾讯云短信服务

@Componentpublic class TencentClient {    @Value("${tencent.secretId}")    private String secretId;    @Value("${tencent.secretKey}")    private String secretKey;        @Bean    public SmsClient client(){        Credential cred = new Credential(secretId, secretKey);        SmsClient smsClient = new SmsClient(cred, "ap-guangzhou");        return smsClient;    }}
@Componentpublic class SendSmsUtils {    @Resource    private TencentClient tencentClient;    @Value("${tencent.sdkAppId}")    private String sdkAppId;    @Value("${tencent.signName}")    private String signName;    @Value("${tencent.templateId}")    private String templateId;        public SendSmsResponse sendSmsResponse (String phone,String code) throws TencentCloudSDKException {        SendSmsRequest req = new SendSmsRequest();                // 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看        req.setSmsSdkAppId(sdkAppId);                // 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看        req.setSignName(signName);                // 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看        req.setTemplateId(templateId);                String[] templateParamSet = {code};        req.setTemplateParamSet(templateParamSet);                String[] phoneNumberSet = new String[]{"+86" + phone};        req.setPhoneNumberSet(phoneNumberSet);                        SmsClient client = tencentClient.client();        return client.SendSms(req);    }}

配置文件

tencent:  secretId: #你的secretId  secretKey: #你的secretKey  sdkAppId: #你的sdkAppId  signName: #你的signName  templateId: #你的templateId
  1. 消息队列的一个用法
  2. ConfirmCallback、ReturnsCallback接口的使用
  3. 腾讯云短信服务的使用
  4. 令牌桶算法的实践

来源地址:https://blog.csdn.net/idogbin/article/details/130444691

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯