Sprintboot+Redis集成腾讯云sms短信验证码服务

550 阅读8分钟

签名申请

在开始之前,需要事前申请签名和创建短信模板,后面代码中会用到,地址在这里 腾讯云->申请签名,申请完等待审核通过即可。

  • 申请签名
  • 创建模板
  • 等待审核

代码实现

pom.xml

首先要安装下腾讯云的包

<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>3.1.62</version>
    <scope>compile</scope>
</dependency>

application.yml

把代码中需要用到的一些重要信息配置到yml中。

sms:
  # 腾讯云中的secretId
  secretId: AKIDb3MYaQ2QjvwmOnyukOq24vc7EWSCVcXE
  # 腾讯云中的secretKey
  secretKey: VpzyjIjy0oNjv0ywthXCiDXcVEkJewer
  # 腾讯云中的appid,在应用列表那里
  appid: 1400795322
  # 签名管理中的前面内容
  signName: 若依页面分层工具网
  # 使用的端点,腾讯云默认sms.tencentcloudapi.com
  endpoint: sms.tencentcloudapi.com
  # 地区
  region: ap-shanghai

sms配置类

通过@ConfigurationProperties读取配置信息并与 bean 绑定,在后面的业务代码中会读取这些配置信息。

@Data
@Component
@ConfigurationProperties(prefix = "sms")
public class MessageConfig {

    /** 腾讯云中的secretId */
    private String secretId;

    /** 腾讯云中的secretKey */
    private String secretKey;

    /** 腾讯云中的appid */
    private String appid;

    /** 签名 */
    private String signName;

    /** 使用的端点 */
    private String endpoint;

    /** 地区 */
    private String region;
}

sms短信实现

使用redis缓存每次发送的验证码,过期时间为10mins;表单提交时,做验证码的有效性和正确性判断。

下面这些代码是获取验证码的主要部分,后面在实现类中直接调该方法即可!

@Service
public class Message {

    @Autowired
    private MessageConfig messageConfig;

    @Autowired
    private RedisCache redisCache;

    public SendSmsResponse smsClient(String phone) throws TencentCloudSDKException {
        /* 必要步骤:
         * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
         * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
         * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
         * 以免泄露密钥对危及你的财产安全。
         * CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/
        Credential cred = new Credential(messageConfig.getSecretId(), messageConfig.getSecretKey());
        // 实例化一个http选项,可选,没有特殊需求可以跳过
        HttpProfile httpProfile = new HttpProfile();
        /* SDK默认使用POST方法。
         * 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
        httpProfile.setReqMethod("POST");
        /* SDK有默认的超时时间,非必要请不要进行调整
         * 如有需要请在代码中查阅以获取最新的默认值 */
        httpProfile.setConnTimeout(60);
        /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务
         * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */
        httpProfile.setEndpoint(messageConfig.getEndpoint());
        /* 非必要步骤:
         * 实例化一个客户端配置对象,可以指定超时时间等配置 */
        ClientProfile clientProfile = new ClientProfile();
        /* SDK 默认用 TC3-HMAC-SHA256 进行签名
         * 非必要请不要修改该字段 */
        clientProfile.setSignMethod("HmacSHA256");
        clientProfile.setHttpProfile(httpProfile);
        SmsClient client = new SmsClient(cred, messageConfig.getRegion(), clientProfile);

        /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
         * 你可以直接查询SDK源码确定接口有哪些属性可以设置
         * 属性可能是基本类型,也可能引用了另一个数据结构
         * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */
        SendSmsRequest req = new SendSmsRequest();
        /* 填充请求参数,这里request对象的成员变量即对应接口的入参
         * 你可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义
         * 基本类型的设置:
         * 帮助链接:
         * 短信控制台: https://console.cloud.tencent.com/smsv2
         * sms helper: https://cloud.tencent.com/document/product/382/3773 */

        /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
        String sdkAppId = messageConfig.getAppid();
        req.setSmsSdkAppid(sdkAppId);
        /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */
        String signName = messageConfig.getSignName();
        req.setSign(signName);

        /* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
        String senderid = "";
        req.setSenderId(senderid);

        /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
        String sessionContext = SecurityUtils.getUsername();
        req.setSessionContext(sessionContext);

        /* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */
        String extendCode = "";
        req.setExtendCode(extendCode);

        /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */
        String templateId = "1771557";
        req.setTemplateID(templateId);

        /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
         * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */
        String[] phoneNumberSet = {"+86" + phone};
        req.setPhoneNumberSet(phoneNumberSet);

        /* 模板参数: 若无模板参数,则设置为空 */
        int code = (int) ((Math.random() * 9 + 1) * 1000); //生成6位随机数字
        String[] templateParamSet = {String.valueOf(code)};
        req.setTemplateParamSet(templateParamSet);

        // redis缓存手机号对应的验证码,这里的code要存入字符串类型,要和前端传入的code类型保持一致!
        redisCache.setCacheObject(Constants.SMS_CODE_KEY + phone, String.valueOf(code), Constants.SMS_EXPIRATION, TimeUnit.MINUTES);

        /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
         * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
        SendSmsResponse res = client.SendSms(req);

        return res;
    }
}

Constants类

    public class Constants{
        /**
         * sms有效期
         */
        public static final Integer SMS_EXPIRATION = 10;
        /**
         * 短信sms redis key
         */
        public static final String SMS_CODE_KEY = "sms_codes:";
    }

redis配置

添加依赖

<!-- redis 缓存操作 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改配置

# Spring配置
spring:
  redis:
    # 地址
    host: xxx.xx.xx.xx
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password: root
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

序列化和反序列化redis的key值

序列化

  • 狭义的层面:将对象转换为字节
  • 广义的层面:将对象转换为指定格式的字符串

反序列化

  • 狭义的层面:将字节转换为对象
  • 广义的层面:将指定格式的字符串转换为对象
序列化和反序列化代码
 
/**
 * redis配置
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

    FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    serializer.setObjectMapper(mapper);

    // 使用StringRedisSerializer来序列化和反序列化redis的key值
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(serializer);

    // Hash的key也采用StringRedisSerializer的序列化方式
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(serializer);

    template.afterPropertiesSet();
    return template;
}

@Bean
public DefaultRedisScript<Long> limitScript()
{
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(limitScriptText());
    redisScript.setResultType(Long.class);
    return redisScript;
}

/**
 * 限流脚本
 */
private String limitScriptText()
{
    return "local key = KEYS[1]\n" +
            "local count = tonumber(ARGV[1])\n" +
            "local time = tonumber(ARGV[2])\n" +
            "local current = redis.call('get', key);\n" +
            "if current and tonumber(current) > count then\n" +
            "    return tonumber(current);\n" +
            "end\n" +
            "current = redis.call('incr', key)\n" +
            "if tonumber(current) == 1 then\n" +
            "    redis.call('expire', key, time)\n" +
            "end\n" +
            "return tonumber(current);";
}




}

}

增加工具类

redis工具类
 
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

/**
 * 缓存基本的对象,Integer、String、实体类等
 *
 * @param key 缓存的键值
 * @param value 缓存的值
 */
public <T> void setCacheObject(final String key, final T value)
{
    redisTemplate.opsForValue().set(key, value);
}

/**
 * 缓存基本的对象,Integer、String、实体类等
 *
 * @param key 缓存的键值
 * @param value 缓存的值
 * @param timeout 时间
 * @param timeUnit 时间颗粒度
 */
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
    redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}

/**
 * 设置有效时间
 *
 * @param key Redis键
 * @param timeout 超时时间
 * @return true=设置成功;false=设置失败
 */
public boolean expire(final String key, final long timeout)
{
    return expire(key, timeout, TimeUnit.SECONDS);
}

/**
 * 设置有效时间
 *
 * @param key Redis键
 * @param timeout 超时时间
 * @param unit 时间单位
 * @return true=设置成功;false=设置失败
 */
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
    return redisTemplate.expire(key, timeout, unit);
}

/**
 * 获得缓存的基本对象。
 *
 * @param key 缓存键值
 * @return 缓存键值对应的数据
 */
public <T> T getCacheObject(final String key)
{
    ValueOperations<String, T> operation = redisTemplate.opsForValue();
    return operation.get(key);
}

/**
 * 删除单个对象
 *
 * @param key
 */
public boolean deleteObject(final String key)
{
    return redisTemplate.delete(key);
}

/**
 * 删除集合对象
 *
 * @param collection 多个对象
 * @return
 */
public long deleteObject(final Collection collection)
{
    return redisTemplate.delete(collection);
}

/**
 * 缓存List数据
 *
 * @param key 缓存的键值
 * @param dataList 待缓存的List数据
 * @return 缓存的对象
 */
public <T> long setCacheList(final String key, final List<T> dataList)
{
    Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
    return count == null ? 0 : count;
}

/**
 * 获得缓存的list对象
 *
 * @param key 缓存的键值
 * @return 缓存键值对应的数据
 */
public <T> List<T> getCacheList(final String key)
{
    return redisTemplate.opsForList().range(key, 0, -1);
}

/**
 * 缓存Set
 *
 * @param key 缓存键值
 * @param dataSet 缓存的数据
 * @return 缓存数据的对象
 */
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
    BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
    Iterator<T> it = dataSet.iterator();
    while (it.hasNext())
    {
        setOperation.add(it.next());
    }
    return setOperation;
}

/**
 * 获得缓存的set
 *
 * @param key
 * @return
 */
public <T> Set<T> getCacheSet(final String key)
{
    return redisTemplate.opsForSet().members(key);
}

/**
 * 缓存Map
 *
 * @param key
 * @param dataMap
 */
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
    if (dataMap != null) {
        redisTemplate.opsForHash().putAll(key, dataMap);
    }
}

/**
 * 获得缓存的Map
 *
 * @param key
 * @return
 */
public <T> Map<String, T> getCacheMap(final String key)
{
    return redisTemplate.opsForHash().entries(key);
}

/**
 * 往Hash中存入数据
 *
 * @param key Redis键
 * @param hKey Hash键
 * @param value 值
 */
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
    redisTemplate.opsForHash().put(key, hKey, value);
}

/**
 * 获取Hash中的数据
 *
 * @param key Redis键
 * @param hKey Hash键
 * @return Hash中的对象
 */
public <T> T getCacheMapValue(final String key, final String hKey)
{
    HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
    return opsForHash.get(key, hKey);
}

/**
 * 删除Hash中的数据
 * 
 * @param key
 * @param mapkey
 */
public void delCacheMapValue(final String key, final String hkey)
{
    HashOperations hashOperations = redisTemplate.opsForHash();
    hashOperations.delete(key, hkey);
}

/**
 * 获取多个Hash中的数据
 *
 * @param key Redis键
 * @param hKeys Hash键集合
 * @return Hash对象集合
 */
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
    return redisTemplate.opsForHash().multiGet(key, hKeys);
}

/**
 * 获得缓存的基本对象列表
 *
 * @param pattern 字符串前缀
 * @return 对象列表
 */
public Collection<String> keys(final String pattern)
{
    return redisTemplate.keys(pattern);
}




}

}

接口

Controller

get请求,传递一个手机号码即可。

@RestController
@RequestMapping("/msg")
public class SmsMessage {
    @Autowired
    private ISmsService ISmsService;

    //发送短信
    @GetMapping("/sendMsg")
    public SendSmsResponse sendMessage(String phone) throws TencentCloudSDKException {
        return ISmsService.sendLoginVeryCodeMessage(phone);
    }
}

Service

public interface ISmsService {

    public SendSmsResponse sendLoginVeryCodeMessage(String phone) throws TencentCloudSDKException;
    
}

impl

只需要在实现类中调用封装好的sms业务代码即可!

@Service
public class SmsServiceImpl implements ISmsService {

    @Autowired
    private Message message;

    public SendSmsResponse sendLoginVeryCodeMessage(String phone) throws TencentCloudSDKException {
        return message.smsClient(phone);
    }
}

验证码校验

此时此刻,已经通过前端页面请求获取到验证码,并且填写至表单中,提交的时候需要对当前的提交的验证码进行校验。之前发送验证码的时候,已经把验证码的内容保存到了redis中;此时只需要把redis缓存中的数据取出来,提交信息中的电话号码为redis的key,根据这个key获取对应的验证码,比对和提交的验证码是否一致即可!

@PostMapping
public AjaxResult add(@RequestBody MerApply merApply) {
    // 获取提交的手机号码
    String smsCode = Constants.SMS_CODE_KEY + merApply.getPhone();
    // 根据手机号码获取redis缓存中的value
    String code = redisCache.getCacheObject(smsCode);
    if (code == null) {
        return error("验证码已失效");
    }
    if (!merApply.getCode().equals(code)) {
        return error("验证码校验不通过");
    }
    merApply.setCreateBy(getUsername());
    return toAjax(this.merApplyService.insert(merApply));
}

案例

去往案例地址>>>

图片.png