SpringBoot腾讯云短信实现验证码

SpringBoot腾讯云短信实现验证码

在学习过程中偶然遇见需要实现验证码功能的需求,于是寻思着将功能抽取出来用于分享学习

业务功能:实现验证码60s,且要求防止用户高频刷验证码(即1min一次不多发)

思路:

  • 调用腾讯云短信API实现验证码功能
  • 因为要求1min中一次不多发,不妨从Redis缓存角度入手,存进去1min,再发送时验证Redis是否有,如果有就不发没有就再发(1min要求同一个用户,需要验证用户是否为同一IP)
  • 注意腾讯云每个号码每天有次数限制,测试过程注意换号码

1 腾讯云短信配置

登录腾讯云,搜索短信,点击第一个进入所需界面

SpringBoot腾讯云短信实现验证码_第1张图片

点击第一个签名管理

SpringBoot腾讯云短信实现验证码_第2张图片

点击创建签名

SpringBoot腾讯云短信实现验证码_第3张图片

进入后点击自用,签名类型为公众号,很多操作官方有提示

SpringBoot腾讯云短信实现验证码_第4张图片

审核成功后,点击正文模板管理

SpringBoot腾讯云短信实现验证码_第5张图片

进入后随便写,会有信息提示

SpringBoot腾讯云短信实现验证码_第6张图片

审核完后进入到访问管理-访问密匙这块功能,这一块有大用

SpringBoot腾讯云短信实现验证码_第7张图片

2 Spring初步集成

2.1 导入依赖


<dependency>
    <groupId>com.tencentcloudapigroupId>
    <artifactId>tencentcloud-sdk-javaartifactId>
    <version>3.1.270version>
dependency>

2.2 存放密匙信息

创建properties或者yml来存放第一阶段最后一步的密钥信息,可以参照下图来配置文件名

SpringBoot腾讯云短信实现验证码_第8张图片

2.3 构建资源类

和秘钥信息做好映射,方便后续获得

@Component
@Data
@PropertySource("classpath:tencentcloud.properties")
@ConfigurationProperties(prefix = "tencent.cloud")
public class TencentCloudProperties {
    private String secretId;
    private String secretKey;
}

2.4 准备工具类

创建一个utils包导入这三个类即可,不是最主要的

package com.imooc.utils;

import javax.servlet.http.HttpServletRequest;

/**
 * 用户获得用户ip的工具类
 */
public class IPUtil {

    /**
     * 获取请求IP:
     * 用户的真实IP不能使用request.getRemoteAddr()
     * 这是因为可能会使用一些代理软件,这样ip获取就不准确了
     * 此外我们如果使用了多级(LVS/Nginx)反向代理的话,ip需要从X-Forwarded-For中获得第一个非unknown的IP才是用户的有效ip。
     * @param request
     * @return
     */
    public static String getRequestIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
package com.imooc.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Title: Redis 工具类
 */
@Component
public class RedisOperator {
   
   @Autowired
   private StringRedisTemplate redisTemplate;

   // Key(键),简单的key-value操作

   /**
    * 判断key是否存在
    * @param key
    * @return
    */
   public boolean keyIsExist(String key) {
      return redisTemplate.hasKey(key);
   }

   /**
    * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
    * 
    * @param key
    * @return
    */
   public long ttl(String key) {
      return redisTemplate.getExpire(key);
   }
   
   /**
    * 实现命令:expire 设置过期时间,单位秒
    * 
    * @param key
    * @return
    */
   public void expire(String key, long timeout) {
      redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
   }
   
   /**
    * 实现命令:increment key,增加key一次
    * 
    * @param key
    * @return
    */
   public long increment(String key, long delta) {
      return redisTemplate.opsForValue().increment(key, delta);
   }

   /**
    * 累加,使用hash
    */
   public long incrementHash(String name, String key, long delta) {
      return redisTemplate.opsForHash().increment(name, key, delta);
   }

   /**
    * 累减,使用hash
    */
   public long decrementHash(String name, String key, long delta) {
      delta = delta * (-1);
      return redisTemplate.opsForHash().increment(name, key, delta);
   }

   /**
    * hash 设置value
    */
   public void setHashValue(String name, String key, String value) {
      redisTemplate.opsForHash().put(name, key, value);
   }

   /**
    * hash 获得value
    */
   public String getHashValue(String name, String key) {
      return (String)redisTemplate.opsForHash().get(name, key);
   }

   /**
    * 实现命令:decrement key,减少key一次
    *
    * @param key
    * @return
    */
   public long decrement(String key, long delta) {
      return redisTemplate.opsForValue().decrement(key, delta);
   }

   /**
    * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
    */
   public Set<String> keys(String pattern) {
      return redisTemplate.keys(pattern);
   }

   /**
    * 实现命令:DEL key,删除一个key
    * 
    * @param key
    */
   public void del(String key) {
      redisTemplate.delete(key);
   }

   // String(字符串)

   /**
    * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
    * 
    * @param key
    * @param value
    */
   public void set(String key, String value) {
      redisTemplate.opsForValue().set(key, value);
   }

   /**
    * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
    * 
    * @param key
    * @param value
    * @param timeout
    *            (以秒为单位)
    */
   public void set(String key, String value, long timeout) {
      redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
   }

   /**
    * 如果key不存在,则设置,如果存在,则报错
    * @param key
    * @param value
    */
   public void setnx60s(String key, String value) {
      redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
   }

   /**
    * 如果key不存在,则设置,如果存在,则报错
    * @param key
    * @param value
    */
   public void setnx(String key, String value) {
      redisTemplate.opsForValue().setIfAbsent(key, value);
   }

   /**
    * 实现命令:GET key,返回 key所关联的字符串值。
    * 
    * @param key
    * @return value
    */
   public String get(String key) {
      return (String)redisTemplate.opsForValue().get(key);
   }

   /**
    * 批量查询,对应mget
    * @param keys
    * @return
    */
   public List<String> mget(List<String> keys) {
      return redisTemplate.opsForValue().multiGet(keys);
   }

   /**
    * 批量查询,管道pipeline
    * @param keys
    * @return
    */
   public List<Object> batchGet(List<String> keys) {

//    nginx -> keepalive
//    redis -> pipeline

      List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
         @Override
         public String doInRedis(RedisConnection connection) throws DataAccessException {
            StringRedisConnection src = (StringRedisConnection)connection;

            for (String k : keys) {
               src.get(k);
            }
            return null;
         }
      });

      return result;
   }


   // Hash(哈希表)

   /**
    * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
    * 
    * @param key
    * @param field
    * @param value
    */
   public void hset(String key, String field, Object value) {
      redisTemplate.opsForHash().put(key, field, value);
   }

   /**
    * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
    * 
    * @param key
    * @param field
    * @return
    */
   public String hget(String key, String field) {
      return (String) redisTemplate.opsForHash().get(key, field);
   }

   /**
    * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
    * 
    * @param key
    * @param fields
    */
   public void hdel(String key, Object... fields) {
      redisTemplate.opsForHash().delete(key, fields);
   }

   /**
    * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
    * 
    * @param key
    * @return
    */
   public Map<Object, Object> hgetall(String key) {
      return redisTemplate.opsForHash().entries(key);
   }

   // List(列表)

   /**
    * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
    * 
    * @param key
    * @param value
    * @return 执行 LPUSH命令后,列表的长度。
    */
   public long lpush(String key, String value) {
      return redisTemplate.opsForList().leftPush(key, value);
   }

   /**
    * 实现命令:LPOP key,移除并返回列表 key的头元素。
    * 
    * @param key
    * @return 列表key的头元素。
    */
   public String lpop(String key) {
      return (String)redisTemplate.opsForList().leftPop(key);
   }

   /**
    * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
    * 
    * @param key
    * @param value
    * @return 执行 LPUSH命令后,列表的长度。
    */
   public long rpush(String key, String value) {
      return redisTemplate.opsForList().rightPush(key, value);
   }

}
package com.imooc.utils;

import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SMSUtils {
    @Autowired
    private TencentCloudProperties tencentCloudProperties;

    public void sendSMS(String phone, String code) throws Exception {
        try {
            /* 必要步骤:
             * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
             * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
             * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
             * 以免泄露密钥对危及你的财产安全。
             * CAM密匙查询获取: https://console.cloud.tencent.com/cam/capi*/
            Credential cred = new Credential(tencentCloudProperties.getSecretId(),
                    tencentCloudProperties.getSecretKey());

            // 实例化一个http选项,可选的,没有特殊需求可以跳过
            HttpProfile httpProfile = new HttpProfile();

//            httpProfile.setReqMethod("POST"); // 默认使用POST

            /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务
             * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */
            httpProfile.setEndpoint("sms.tencentcloudapi.com");

            // 实例化一个client选项
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);
            // 实例化要请求产品的client对象,clientProfile是可选的
            SmsClient client = new SmsClient(cred, "ap-nanjing", clientProfile);

            // 实例化一个请求对象,每个接口都会对应一个request对象
            SendSmsRequest req = new SendSmsRequest();
            String[] phoneNumberSet1 = {"+86" + phone};//电话号码
            req.setPhoneNumberSet(phoneNumberSet1);
            req.setSmsSdkAppId("1400782012");   // 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId
            req.setSignName("tgywata公众号");         // 签名
            req.setTemplateId("1646128");       // 模板id:必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看

            /* 模板参数(自定义占位变量): 若无模板参数,则设置为空 */
            String[] templateParamSet1 = {code};
            req.setTemplateParamSet(templateParamSet1);

            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
            SendSmsResponse resp = client.SendSms(req);
            // 输出json格式的字符串回包
//            System.out.println(SendSmsResponse.toJsonString(resp));
        } catch (TencentCloudSDKException e) {
            System.out.println(e.toString());
        }
    }

    /*public static void main(String[] args) {
        try {
            new SMSUtils().sendSMS("18812345612", "7896");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }*/
}

3 整合Redis

3.1 引入依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

3.2 配置yml

spring:
  redis:
    host: 192.168.1.61
    port: 6379
    database: 0
    password: itzixi

3.3 创建父类

这一步可以省略,主要是让实际的Controller类没那么复杂

import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;

public class BaseInfoProperties {

    @Autowired
    public RedisOperator redis;

    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";
    public static final String REDIS_USER_INFO = "redis_user_info";
}

3.4 创建测试API

package com.imooc.controller;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Api(tags = "PassportController 通行证接口模块")
@RequestMapping("passport")
@RestController
public class PassportController extends BaseInfoProperties {
    // /passport/getSMSCode?mobile=
    @Autowired
    private SMSUtils smsUtils;

    @PostMapping("getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request) throws Exception {

        // 手机号为空不做操作
        if (StringUtils.isBlank(mobile)) {
            return GraceJSONResult.ok();
        }

        // 获得用户ip
        String userIp = IPUtil.getRequestIp(request);
        // 根据用户ip进行限制 限制用户在60秒之内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 随机生成验证码
        String code = (int) ((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS(mobile, code);

        // 控制台打印号码日志 防止接收不到
        log.info(code);

        // 把验证码放入到redis中 用于后续的验证
        redis.set(MOBILE_SMSCODE + ":" + mobile, code, 30 * 60); // 三十分钟失效

        return GraceJSONResult.ok();
    }

}

4 优化API

4.1 创建拦截器

创建拦截器主要用于实现==同一用户1min之内验证频率过高的情况

package com.imooc.intercepter;

import com.imooc.controller.BaseInfoProperties;
import com.imooc.utils.IPUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class PassportInterceptor extends BaseInfoProperties implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获得用户的ip
        String userIp = IPUtil.getRequestIp(request);

        // 得到是否存在的判断
        boolean keyIsExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);

        if (keyIsExist) {
            log.info("短信发送频率太大!");
            return false; // 拒绝通行 请求拦截
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

4.2 配置拦截器

package com.imooc;

import com.imooc.intercepter.PassportInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Bean
    public PassportInterceptor passportInterceptor() {
        return new PassportInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(passportInterceptor()).
                addPathPatterns("/passport/getSMSCode");
    }
}

5 实验

SpringBoot腾讯云短信实现验证码_第9张图片image-20221222201821306

你可能感兴趣的:(业务小功能,SpringBoot,Java学习,腾讯云,java,spring,boot,后端,改行学it)