目前,系统集成短信似乎是必不可少的组成部分。由于各种云平台提供不同的短信渠道,我们增加了多租户和多渠道的短信验证码,并增加了配置项目,使系统能够支持多个云平台提供的短信服务。以阿里巴巴云和腾讯云为例,集成短信通知服务。
Spring Boot常用的集成短信方式有:- API短信服务提供商API服务提供商 主要短信服务提供商提供的API接口可通过HTTP/HTTPS协议调用。例如,阿里巴巴云的SMS服务和腾讯云的短信服务都提供Java SDK和API文档可以与Spring一起使用 Boot集成使用。
- SDKK第三方短信平台 第三方短信平台提供的SDK可用于短信发送。例如,Jiguang提供的SMS SDK,它可以很容易地集成到Spring 在Boot项目中。
- 自建短信网关 使用第三方短信网关提供商的API通过自建短信网关发送短信。自建短信网关可以提高短信发送的控制和可靠性,但需要一定的技术和资源投入。
- 使用现成的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); }}