当前位置: 首页 > 图灵资讯 > 技术篇> SpringCloud微服务实战——搭建企业级开发框架(二十五):集成短信通知服务

SpringCloud微服务实战——搭建企业级开发框架(二十五):集成短信通知服务

来源:图灵教育
时间:2023-10-20 17:54:34

  目前,系统集成短信似乎是必不可少的组成部分。由于各种云平台提供不同的短信渠道,我们增加了多租户和多渠道的短信验证码,并增加了配置项目,使系统能够支持多个云平台提供的短信服务。以阿里巴巴云和腾讯云为例,集成短信通知服务。

Spring Boot常用的集成短信方式有:
  1. API短信服务提供商API服务提供商  主要短信服务提供商提供的API接口可通过HTTP/HTTPS协议调用。例如,阿里巴巴云的SMS服务和腾讯云的短信服务都提供Java SDK和API文档可以与Spring一起使用 Boot集成使用。
  2. SDKK第三方短信平台  第三方短信平台提供的SDK可用于短信发送。例如,Jiguang提供的SMS SDK,它可以很容易地集成到Spring 在Boot项目中。
  3. 自建短信网关  使用第三方短信网关提供商的API通过自建短信网关发送短信。自建短信网关可以提高短信发送的控制和可靠性,但需要一定的技术和资源投入。
  4. 使用现成的Spring Boot短信模块  Spring Boot社区提供了一些开源短信模块,如spring-boot-starter-sms,Spring可以快速集成 在Boot项目中,实现短信发送功能。

1、在Giteg-Platform中新建gitegggg-platform-Sms基础工程,定义抽象方法和配置SmssendService发送短信抽象接口:

/** * 短信发送接口 */public interface SmsSendService {    /**     * 发送单个短信     * @param smsData     * @param phoneNumber     * @return     */    default SmsResponse sendSms(SmsData smsData, String phoneNumber){        if (StrUtil.isEmpty(phoneNumber)) {            return new SmsResponse();        }        return this.sendSms(smsData, Collections.singletonList(phoneNumber));    }    /**     * 群发短信     * @param smsData     * @param phoneNumbers     * @return     */    SmsResponse sendSms(SmsData smsData, Collection<String> phoneNumbers);    }

Smsresultcodenum定义短信发送结果

/** * @ClassName: ResultCodeEnum * @Description: 自定义返回码枚举 * @author GitEgg * @date 2020年09月19日 下午11:49:45 */@Getter@AllArgsConstructorpublic enum SmsResultCodeEnum {    /**     * 成功     */    SUCCESS(200, "操作成功"),    /**     * 系统很忙,请稍后重试。     */    ERROR(429, "短信发送失败,请稍后重试"),    /**     * 系统错误     */    PHONE_NUMBER_ERROR(500, "手机号错误");    public int code;    public String msg;}

2、新建gitegg-platform-sms-aliyun项目实现阿里云短信发送接口AliyunSmsProperties配置类

@Data@Component@ConfigurationProperties(prefix = "sms.aliyun")public class AliyunSmsProperties {    /**     * product     */    private String product = "Dysmsapi";    /**     * domain     */    private String domain = "dysmsapi.aliyuncs.com";    /**     * regionId     */    private String regionId = "cn-hangzhou";    /**     * accessKeyId     */    private String accessKeyId;    /**     * accessKeySecret     */    private String accessKeySecret;    /**     * 短信签名     */    private String signName;}

阿里云短信发送接口AliyunSendServiceImpl实现类别实现

/** * 发送阿里云短信 */@Slf4j@AllArgsConstructorpublic class AliyunSmsSendServiceImpl implements SmsSendService {    private static final String successCode = "OK";    private final AliyunSmsProperties properties;    private final IAcsClient acsClient;    @Override    public SmsResponse sendSms(SmsData smsData, Collection<String> phoneNumbers) {        SmsResponse smsResponse = new SmsResponse();        SendSmsRequest request = new SendSmsRequest();        request.setSysMethod(MethodType.POST);        request.setPhoneNumbers(StrUtil.join(",", phoneNumbers));        request.setSignName(properties.getSignName());        request.setTemplateCode(smsData.getTemplateId());        request.setTemplateParam(JsonUtils.mapToJson(smsData.getParams()));        try {            SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);            if (null != sendSmsResponse && !StringUtils.isEmpty(sendSmsResponse.getCode())) {                if (this.successCode.equals(sendSmsResponse.getCode())) {                    smsResponse.setSuccess(true);                } else {                    log.error("Send Aliyun Sms Fail: [code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage());                }                smsResponse.setCode(sendSmsResponse.getCode());                smsResponse.setMessage(sendSmsResponse.getMessage());            }        } catch (Exception e) {            e.printStackTrace();            log.error("Send Aliyun Sms Fail: {}", e);            smsResponse.setMessage("Send Aliyun Sms Fail!");        }        return smsResponse;    }}

3、新建gitegg-platform-sms-tencent项目实现腾讯云短信发送接口tencentSmsproperties配置类别

@Data@Component@ConfigurationProperties(prefix = "sms.tencent")public class TencentSmsProperties {    /* 填写要求参数,这里 request 对象的成员变量是对应接口的入参     * 您可以通过官网接口文档或跳转到 request 查看请求参数的定义     * 设置基本类型:     * 帮助链接:     * 短信控制台:https://www.tulingxueyuan.cn/d/file/p/20231020/1r4coom4bag     * sms helper:https://www.tulingxueyuan.cn/d/file/p/20231020/bzpabwzrxz5 */    /* 短信应用 ID: 在 [短信控制台] 添加应用后产生的实际应用 SDKAppID,例如1400006666 */    private String SmsSdkAppId;    /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通,请联系 [sms helper] */    private String senderId;    /* 短信码号扩展号: 默认未开通,如果需要打开请联系 [sms helper] */    private String extendCode;    /**     * 短信签名     */    private String signName;}

Tencentssendserviceimpl腾讯云短信发送接口实现类实现

/** * 发送腾讯云短信 */@Slf4j@AllArgsConstructorpublic class TencentSmsSendServiceImpl implements SmsSendService {    private static final String successCode = "Ok";    private final TencentSmsProperties properties;    private final SmsClient client;    @Override    public SmsResponse sendSms(SmsData smsData, Collection<String> phoneNumbers) {        SmsResponse smsResponse = new SmsResponse();        SendSmsRequest request = new SendSmsRequest();        request.setSmsSdkAppid(properties.getSmsSdkAppId());        /* 短信签名内容: 使用 UTF-8 编码,必须填写已批准的签名,并可登录 [短信控制台] 查看签名信息 */        request.setSign(properties.getSignName());        /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通,请联系 [sms helper] */        if (!StringUtils.isEmpty(properties.getSenderId()))        {            request.setSenderId(properties.getSenderId());        }        request.setTemplateID(smsData.getTemplateId());        /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号码]         * 例如+8613711112222, 前面有一个+号 ,86是国家码,137111222是手机号,最多不超过200个手机号*/        String[] phoneNumbersArray = (String[]) phoneNumbers.toArray();        request.setPhoneNumberSet(phoneNumbersArray);        /* 模板参数: 若没有模板参数,设置为空*//        String[] templateParams = new String[]{};        if (!CollectionUtils.isEmpty(smsData.getParams())) {            templateParams = (String[]) smsData.getParams().values().toArray();        }        request.setTemplateParamSet(templateParams);        try {            /* 通过 client 对象调用 SendSms 方法发起请求。请注意请求方法名对应于请求对象             * 返回的 res 是一个 SendSmsResponse 类的实例,对应于请求对象 */            SendSmsResponse sendSmsResponse = client.SendSms(request);            //如果是批量发送,腾讯云短信将返回每条短信的发送状态,在这里默认返回第一条短信的状态            if (null != sendSmsResponse && null != sendSmsResponse.getSendStatusSet()) {                SendStatus sendStatus = sendSmsResponse.getSendStatusSet()[0];                if (this.successCode.equals(sendStatus.getCode()))                {                    smsResponse.setSuccess(true);                }                else                {                    smsResponse.setCode(sendStatus.getCode());                    smsResponse.setMessage(sendStatus.getMessage());                }            }        } catch (Exception e) {            e.printStackTrace();            log.error("Send Aliyun Sms Fail: {}", e);            smsResponse.setMessage("Send Aliyun Sms Fail!");        }        return smsResponse;    }}

4、在Giteg-Cloud中建立新的业务调用方法,这里要考虑不同的租户调用不同的短信配置发送短信,所以新建的SmsFactory短信接口实例化工厂,根据不同的租户实例发送不同的短信接口,以实例化com为例.gitegg.service.extension.sms.factory.以SmsaliyunFactory为例,实例操作,在实际使用中,需要从租户的短信配置中配置与租户的对应关系。

@Componentpublic class SmsFactory {    private final ISmsTemplateService smsTemplateService;    /**     * SmsSendService 缓存     */    private final Map<Long, SmsSendService> SmsSendServiceMap = new ConcurrentHashMap<>();    public SmsFactory(ISmsTemplateService smsTemplateService) {        this.smsTemplateService = smsTemplateService;    }    /**     * 获取 SmsSendService     *     * @param smsTemplateDTO 短信模板     * @return SmsSendService     */    public SmsSendService getSmsSendService(SmsTemplateDTO smsTemplateDTO) {        ///根据channelID获取相应的短信服务接口,channelId是唯一的,每个租户都有自己的chanelID        Long channelId = smsTemplateDTO.getChannelId();        SmsSendService smsSendService = SmsSendServiceMap.get(channelId);        if (null == smsSendService) {            Class cls = null;            try {                cls = Class.forName("com.gitegg.service.extension.sms.factory.SmsAliyunFactory");                Method staticMethod = cls.getDeclaredMethod("getSmsSendService", SmsTemplateDTO.class);                smsSendService = (SmsSendService) staticMethod.invoke(cls,smsTemplateDTO);                SmsSendServiceMap.put(channelId, smsSendService);            } catch (ClassNotFoundException | NoSuchMethodException e) {                e.printStackTrace();            } catch (IllegalAccessException e) {                e.printStackTrace();            } catch (InvocationTargetException e) {                e.printStackTrace();            }        }        return smsSendService;    }}
/** * 阿里云短信服务接口工厂 */public class SmsAliyunFactory {    public static SmsSendService getSmsSendService(SmsTemplateDTO sms) {        AliyunSmsProperties aliyunSmsProperties = new AliyunSmsProperties();        aliyunSmsProperties.setAccessKeyId(sms.getSecretId());        aliyunSmsProperties.setAccessKeySecret(sms.getSecretKey());        aliyunSmsProperties.setRegionId(sms.getRegionId());        aliyunSmsProperties.setSignName(sms.getSignName());        IClientProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret());        IAcsClient acsClient = new DefaultAcsClient(profile);        return new AliyunSmsSendServiceImpl(aliyunSmsProperties, acsClient);    }}
/** * 腾讯云短信服务接口工厂 */public class SmsTencentFactory {    public static SmsSendService getSmsSendService(SmsTemplateDTO sms) {        TencentSmsProperties tencentSmsProperties = new TencentSmsProperties();        tencentSmsProperties.setSmsSdkAppId(sms.getSecretId());        tencentSmsProperties.setExtendCode(sms.getSecretKey());        tencentSmsProperties.setSenderId(sms.getRegionId());        tencentSmsProperties.setSignName(sms.getSignName());        /* 必要步骤:         * 实例化一个认证对象,需要输入腾讯云账户密钥进入参与 secretId 和 secretKey         * 这个例子是从环境变量中读取的,需要在环境变量中提前设置这两个值         * 您还可以直接在代码中写入密钥对,但要小心泄漏,不要复制、上传或与他人分享代码         * CAM 密钥查询:https://www.tulingxueyuan.cn/d/file/p/20231020/z5dhhcpvqp0         */        Credential cred = new Credential(sms.getSecretId(), sms.getSecretKey());        // 实例化一个 http 选项,可选,在没有特殊需要的情况下可以跳过        HttpProfile httpProfile = new HttpProfile();        // 设置代理/////        httpProfile.setProxyHost("host");//        httpProfile.setProxyPort(port);        /* SDK 默认使用 POST 方法。         * 如需使用 GET 这里可以设置方法,但是 GET 该方法不能处理较大的请求 */        httpProfile.setReqMethod("POST");        /* SDK 有默认的超时间,非必要时请不要进行调整         * 如有必要,请访问代码以获得最新的默认值 */        httpProfile.setConnTimeout(60);        /* SDK 通常不需要指定域名就可以自动指定域名,但是,在访问金融区时,必须手动指定域名         * 例如 SMS 上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */        if (!StringUtils.isEmpty(sms.getRegionId()))        {            httpProfile.setEndpoint(sms.getRegionId());        }        /* 不必要的步骤:         * 实例化客户端配置对象,可指定超时间等配置 */        ClientProfile clientProfile = new ClientProfile();        /* SDK 默认用 TC3-HMAC-SHA256 进行签名         * 非必要请不要修改这个字段 */        clientProfile.setSignMethod("HMACSHA256");        clientProfile.setHttpProfile(httpProfile);        /* 实例化 SMS 的 client 对象         * 第二个参数是区域信息,可直接填写字符串 ap-guangzhou,或引用预设常量 */        SmsClient client = new SmsClient(cred, "",clientProfile);        return new TencentSmsSendServiceImpl(tencentSmsProperties, client);    }}

5、定义短信发送接口,实现ISmsService业务短信发送接口定义

/** * <p> * 短信发送界面定义 * </p> * * @author GitEgg * @since 2021-01-25 */public interface ISmsService {    /**     * 发送短信     *     * @param smsCode     * @param smsData     * @param phoneNumbers     * @return     */    SmsResponse sendSmsNormal(String smsCode, String smsData, String phoneNumbers);    /**     * 发送短信验证码     *     * @param smsCode     * @param phoneNumber     * @return     */    SmsResponse sendSmsVerificationCode( String smsCode, String phoneNumber);    /**     * 验证短信验证码     *     * @param smsCode     * @param phoneNumber     * @return     */    boolean checkSmsVerificationCode(String smsCode, String phoneNumber, String verificationCode);}

SmsServiceImpl 短信发送接口实现类

/** * <p> * 短信发送接口实现类 * </p> * * @author GitEgg * @since 2021-01-25 */@Slf4j@Service@RequiredArgsConstructor(onConstructor_ = @Autowired)public class SmsServiceImpl implements ISmsService {    private final SmsFactory smsFactory;    private final ISmsTemplateService smsTemplateService;    private final RedisTemplate redisTemplate;    @Override    public SmsResponse sendSmsNormal(String smsCode, String smsData, String phoneNumbers) {        SmsResponse smsResponse = new SmsResponse();        try {            QuerySmsTemplateDTO querySmsTemplateDTO = new QuerySmsTemplateDTO();            querySmsTemplateDTO.setSmsCode(smsCode);            //获取有关短信code的信息,根据mybatiss,租户信息将根据mybatiss 获取plus插件            SmsTemplateDTO smsTemplateDTO = smsTemplateService.querySmsTemplate(querySmsTemplateDTO);            ObjectMapper mapper = new ObjectMapper();            Map smsDataMap = mapper.readValue(smsData, Map.class);            List<String> phoneNumberList =  JsonUtils.jsonToList(phoneNumbers, String.class);            SmsData smsDataParam = new SmsData();            smsDataParam.setTemplateId(smsTemplateDTO.getTemplateId());            smsDataParam.setParams(smsDataMap);            SmsSendService smsSendService = smsFactory.getSmsSendService(smsTemplateDTO);            smsResponse = smsSendService.sendSms(smsDataParam, phoneNumberList);        } catch (Exception e) {            smsResponse.setMessage("短信发送失败");            e.printStackTrace();        }        return smsResponse;    }    @Override    public SmsResponse sendSmsVerificationCode(String smsCode, String phoneNumber) {        String verificationCode = RandomUtil.randomNumbers(6);        Map<String, String> smsDataMap = new HashMap<>();        smsDataMap.put(SmsConstant.SMS_CAPTCHA_TEMPLATE_CODE, verificationCode);        List<String> phoneNumbers = Arrays.asList(phoneNumber);        SmsResponse smsResponse = this.sendSmsNormal(smsCode, JsonUtils.mapToJson(smsDataMap), JsonUtils.listToJson(phoneNumbers));        if (null != smsResponse && smsResponse.isSuccess()) {            // 将短信验证码存入redis,并将过期时间设置为5分钟            redisTemplate.opsForValue().set(SmsConstant.SMS_CAPTCHA_KEY + smsCode + phoneNumber, verificationCode, 30,                    TimeUnit.MINUTES);        }        return smsResponse;    }    @Override    public boolean checkSmsVerificationCode(String smsCode, String phoneNumber, String verificationCode) {        String verificationCodeRedis = (String) redisTemplate.opsForValue().get(SmsConstant.SMS_CAPTCHA_KEY + smsCode + phoneNumber);        if (!StrUtil.isAllEmpty(verificationCodeRedis, verificationCode) && verificationCode.equalsIgnoreCase(verificationCodeRedis)) {            return true;        }        return false;    }}

6、新建Smsfeign类,供其他微服务调用发送短信

/** * @ClassName: SmsFeign * @Description: Smsfeign前端控制器 * @author gitegg * @date 2019年5月18日 下午4:03:58 */@RestController@RequestMapping(value = "/feign/sms")@RequiredArgsConstructor(onConstructor_ = @Autowired)@Api(value = "Smsfeign|提供微服务调用接口")@RefreshScopepublic class SmsFeign {    private final ISmsService smsService;    @GetMapping(value = "/send/normal")    @ApiOperation(value = "发送普通短信", notes = "发送普通短信")    Result<Object> sendSmsNormal(@RequestParam("smsCode") String smsCode, @RequestParam("smsData") String smsData, @RequestParam("phoneNumbers") String phoneNumbers) {        SmsResponse smsResponse = smsService.sendSmsNormal(smsCode, smsData, phoneNumbers);        return Result.data(smsResponse);    }    @GetMapping(value = "/send/verification/code")    @ApiOperation(value = "发送短信验证码", notes = "发送短信验证码")    Result<Object> sendSmsVerificationCode(@RequestParam("smsCode") String smsCode, @RequestParam("phoneNumber") String phoneNumber) {        SmsResponse smsResponse = smsService.sendSmsVerificationCode(smsCode, phoneNumber);        return Result.data(smsResponse);    }    @GetMapping(value = "/check/verification/code")    @ApiOperation(value = "验证短信验证码", notes = "验证短信验证码")    Result<Boolean> checkSmsVerificationCode(@RequestParam("smsCode") String smsCode, @RequestParam("phoneNumber") String phoneNumber, @RequestParam("verificationCode") String verificationCode) {        boolean checkResult = smsService.checkSmsVerificationCode(smsCode, phoneNumber, verificationCode);        return Result.data(checkResult);    }}