【已解决】Java 项目中接入天翼云短信推送接口

🎉工作场景中遇到这样一个需求:在项目中接入天翼云短信推送接口,用于向用户推送短信消息,如短信验证码、系统信息推送以及推广信息等。

通过在天翼云短信服务官网进行一系列的调研之后,得到接入天翼云短信服务接口的基本步骤为:

  1. 注册账号,并进行实名认证
  2. 开通短信服务:购买短信套餐
  3. 创建短信内容:1、申请短信签名。2、申请短信模板。
  4. 调用API,发送短信

其中,1-3 步骤不涉及代码编写属于前期的准备工作,只有第4个步骤需要进行代码的编写。因此,接入的主要工作也是在第4个步骤,下面将重点介绍如何在代码层面调用API,实现发送短信功能。

参照天翼云短信服务API 概览文档,发送短信接口请求参数如下所示:

名称类型是否必要示例值描述
actionStringSendSms系统规定参数。取值:SendSms
phoneNumberString13301110000接收短信的手机号码,格式:国内短信:无任何前缀的11位手机号码,如1355286****
signNameString中国电信短信签名名称。请在控制台的签名管理页签下签名名称一列查看。说明:必须是已添加、并通过审核的短信签名
templateCodeStringSMS73419576145短信模板ID。请在控制台的模板管理页签下模板CODE一列查看。说明:必须是已添加、并通过审核的短信模板
templateParamString{“code”:“1111”}短信模板变量对应的实际值,JSON格式。说明:如果JSON中需要带换行符,请参照标准的JSON协议处理
extendCodeString90999上行短信扩展码,上行短信,指发送给通信服务提供商的短信,用于定制某种服务、完成查询,或是办理某种业务等,需要收费的,按运营商普通短信资费进行扣费。说明: 无特殊需要此字段的用户请忽略此字段
sessionIdString123456客户自带短信标识,在状态报告中会原样返回。说明: 无特殊需要此字段的用户请忽略此字段

请求参数示例

{
    "action": "SendSms",
    "signName": "中国电信",
    "phoneNumber": "13301110000",
    "templateCode": "SMS73419576145",
    "templateParam": "{\"code\":\"123456\",\"time\":\"1\"}",
    "extendCode": "123"
}

发送短信接口返回参数如下表所示

名称类型示例值描述
codeStringOK请求状态码。返回OK代表请求成功。错误码见错误码列表
messageStringOK状态码描述
requstIdStringF655A8D5B967请求ID

返回参数示例

{
  "code": "OK",
  "message": "success",
  "requestId": "TxxfZdCz0sbhddVx"
}

对于每一次 HTTP 或 HTTPS 协议请求,天翼云服务方都会根据访问中的签名信息验证访问请求者身份。因此,对于每次接口的调用请求,都需要由 accessKeysecurityKey 进行请求签名验证实现之后才能实现。请求签名的具体步骤如下所示

步骤一:获取 accessKey 和 securityKey:可以在天翼云官网—>个人中心—>基本信息中查看。

步骤二:构造时间戳:构造一个 eop-date 的时间戳,格式为 yyyymmddTHHMMSSZ, 简单来说就是“年月日T时分秒Z”。

步骤三:构造请求流水号:构造一个 ctyun-eop-request-id 的流水号,最好为每次请求不同,可以简单的使用 UUID.

步骤四:构造待签名字符串:

  1. 构造进行签名的Header:以 headerName:headerValue 来一个一个通过"\n"拼接起来,强制要求 ctyun-eop-request-ideop-date 这个头作为 header 中的一部分。将待签名算法的 header 需要进行排序(headerName 以英文字母的顺序来排序),将排序后得到的列表进行遍历组装成待签名的 header.
  2. 构造待签名的Query:Query 以 & 作为拼接,keyvalue 以"="连接,排序规则使用26个英文字母的顺序来排序,Query 参数全部都需要进行签名。
  3. 构造待签名的Body:传待发送的 Body 参数进行 sha256 摘要,对摘要出来的结果转十六进制。
  4. 将待签名的Header、Query、Body 通过"\n"进行连接

步骤五:构造签名

  1. 先将 securityKey 作为密钥,eop-date 作为数据,根据 hmacsha256 加密算法算出 kTime.
  2. kTime 作为密钥,accessKey 作为数据,根据 hmacsha256 加密算法算出 kAk.
  3. kAk 作为密钥,eop-date 的年月日值(前8位)作为数据,根据 hmacsha256 加密算法算出 kDate.
  4. kDate 作为密钥,步骤二的待签名字符串作为数据,根据 hmacsha256 加密算法算法签名并转化为 BASE64编码算出 signature.

步骤六:构造请求头

  1. eop-date 作为 key,步骤二的结果作为 value 加入 http 请求头中。
  2. ctyun-eop-request-id 作为 key,步骤三的结果作为 value 加入 http 请求头中。
  3. eop-Authorization 作为 key,通过字符串拼接的方式将 accessKeyHeadersignature 通过空格进行拼接,并将结果作为 value 加入 http 请求头中。

根据官网给出的Demo代码以及结合项目中的实际应用,抽出去除业务相关代码,给出以下一个可以调试成功并较为简洁的代码示例如下:

封装参数的 Java 类 SendCtyunSmsRequest

public class SendCtyunSmsRequest {

    /**
     * 接收短信的手机号码,格式:国内短信:无任何前缀的11位手机号码
     */
    private String phoneNumber;

    /**
     * 短信签名名称
     */
    private String signName;

    /**
     * 短信模板ID
     */
    private String templateCode;

    /**
     * 短信模板变量对应的实际值,JSON格式。说明:如果JSON中需要带换行符,请参照标准的JSON协议处理
     */
    private String templateParam;

    /**
     * 上行短信扩展码,上行短信,指发送给通信服务提供商的短信,用于定制某种服务、完成查询,或是办理某种业务等,需要收费的按运营商普通短信资费进行扣费
     */
    private String extendCode;

    /**
     * 客户自带短信标识,在状态报告中会原样返回
     */
    private String sessionId;

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getSignName() {
        return signName;
    }

    public void setSignName(String signName) {
        this.signName = signName;
    }

    public String getTemplateCode() {
        return templateCode;
    }

    public void setTemplateCode(String templateCode) {
        this.templateCode = templateCode;
    }

    public String getTemplateParam() {
        return templateParam;
    }

    public void setTemplateParam(String templateParam) {
        this.templateParam = templateParam;
    }

    public String getExtendCode() {
        return extendCode;
    }

    public void setExtendCode(String extendCode) {
        this.extendCode = extendCode;
    }

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}

发送短信功能测试类 Test

public class Test {
    public static void main(String[] args) {
        SendCtyunSmsRequest sendCtyunSmsRequest = new SendCtyunSmsRequest();
        sendCtyunSmsRequest.setPhoneNumber("13301110000");
        sendCtyunSmsRequest.setSignName("中国电信");
        sendCtyunSmsRequest.setTemplateCode("SMS73419576145");
        sendCtyunSmsRequest.setTemplateParam("{\"code\":\"666666\"}");
        sendSms(sendCtyunSmsRequest);
    }

    private static void sendSms(SendCtyunSmsRequest sendCtyunSmsRequest) {
        // 获取accessKey和securityKey
        String accessKey = "test";  // 填写控制台->个人中心->用户AccessKey->查看->AccessKey
        String securityKey ="test"; // 填写控制台->个人中心->用户AccessKey->查看->SecurityKey
        
        // 构造body请求参数
        Map<String, String> params = buildParams(sendCtyunSmsRequest);
        String body = JsonTool.serialize(params);
        try {
            // 构造时间戳
            SimpleDateFormat timeFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            Date now = new Date();
            String signatureTime = timeFormat.format(now);
            String signatureDate = dateFormat.format(now);

            // 构造请求流水号
            String uuId = UUID.randomUUID().toString();

            // 构造待签名字符串
            String campHeader = String.format("ctyun-eop-request-id:%s\neop-date:%s\n", uuId, signatureTime);
            // header的key按照26字母进行排序, 以&作为连接符连起来
            URL url = new URL("https://sms-global.ctapi.ctyun.cn/sms/api/v1");
            String query = url.getQuery();
            StringBuilder afterQuery = new StringBuilder();
            if (query != null) {
                String[] param = query.split("&");
                Arrays.sort(param);
                for (String str : param) {
                    if (afterQuery.length() < 1)
                        afterQuery.append(str);
                    else
                        afterQuery.append("&").append(str);
                }
            }

            // 报文原封不动进行sha256摘要
            String calculateContentHash = getSHA256(body);
            String signatureStr = campHeader + "\n" + afterQuery + "\n" + calculateContentHash;

            // 构造签名
            byte[] kTime = hmacSHA256(signatureTime.getBytes(), securityKey.getBytes());
            byte[] kAk = hmacSHA256(accessKey.getBytes(), kTime);
            byte[] kDate = hmacSHA256(signatureDate.getBytes(), kAk);
            String signature = Base64.getEncoder().encodeToString(hmacSHA256(signatureStr.getBytes(StandardCharsets.UTF_8), kDate));

            // 构造请求头
            HttpPost httpPost = new HttpPost(String.valueOf(url));
            httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
            httpPost.setHeader("ctyun-eop-request-id", uuId);
            httpPost.setHeader("Eop-date", signatureTime);
            String signHeader = String.format("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", accessKey, signature);
            httpPost.setHeader("Eop-Authorization", signHeader);

            httpPost.setEntity(new StringEntity(body, ContentType.create("application/json", "utf-8")));

            try (CloseableHttpClient httpClient = HttpClients.createDefault();
                 CloseableHttpResponse response = httpClient.execute(httpPost) ) {
                String result = EntityUtils.toString(response.getEntity(), "utf-8");
                System.out.println("返回结果:" + result);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    /**
     * 构造请求参数
     * @param sendCtyunSmsRequest 请求参数
     * @return Map
     */
    private static Map<String, String> buildParams(SendCtyunSmsRequest sendCtyunSmsRequest) {
        Map<String, String> params = new HashMap<>(16);
        params.put("action", "SendSms");
        params.put("phoneNumber", sendCtyunSmsRequest.getPhoneNumber());
        params.put("signName", sendCtyunSmsRequest.getSignName());
        params.put("templateCode", sendCtyunSmsRequest.getTemplateCode());
        params.put("templateParam", sendCtyunSmsRequest.getTemplateParam());
        params.put("extendCode", sendCtyunSmsRequest.getExtendCode());
        params.put("sessionId", sendCtyunSmsRequest.getSessionId());
        return params;
    }

    private static String toHex(byte[] data) {
        StringBuilder sb = new StringBuilder(data.length * 2);
        byte[] var2 = data;
        int var3 = data.length;
        for (int var4 = 0; var4 < var3; ++var4) {
            byte b = var2[var4];
            String hex = Integer.toHexString(b);
            if (hex.length() == 1) {
                sb.append("0");
            } else if (hex.length() == 8) {
                hex = hex.substring(6);
            }
            sb.append(hex);
        }
        return sb.toString().toLowerCase(Locale.getDefault());
    }

    private static String getSHA256(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(text.getBytes(StandardCharsets.UTF_8));
            return toHex(md.digest());
        } catch (NoSuchAlgorithmException var3) {
            return null;
        }
    }

    private static byte[] hmacSHA256(byte[] data, byte[] key) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key, "HmacSHA256"));
            return mac.doFinal(data);
        } catch (Exception e) {
            return new byte[0];
        }
    }
}

调用接口成功后的返回结果

{
	"code":30021,
	"message":"No Remain",
	"requestId":"cfcbiirc4v106cdb3mk0"
}

由于暂时没有购买短信套餐包,因此返回的结果提示是没有短信余量,但是以上的返回结果表示已经调用天翼云短信发送接口成功。 返回结果中的 code 也是天翼云短信接口那边返回的,可以在天翼云短信服务官网错误码列表 中查看到。

最后,需要注意的是,经过本人前期的一些调研,发现天翼云短信服务官方文档因为历史原因,相关信息未能及时更新,如发送短信接口的请求参数、返回参数示例中的字段大小写问题,应该统一首字母小写,而官网文档给出的是大写,可以及时联系天翼云的客服人员咨询。

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Java可以使用多线程来实现一个推送微信、邮箱和短信接口。具体实现方式可以采用创建多个线程分别负责微信、邮箱和短信推送,这样可以使得三种推送方式可以并行执行。下面是一个简单的实现示例: ``` public class PushService { public void push(String message) { Thread wechatThread = new Thread(() -> { // 推送微信 }); Thread emailThread = new Thread(() -> { // 推送邮箱 }); Thread smsThread = new Thread(() -> { // 推送短信 }); wechatThread.start(); emailThread.start(); smsThread.start(); } } ``` 不过这个例子采用的是线程实现,线程有时间和资源限制,我们可以用线程池来替代,可以提高性能。 java有很多的实现线程池的方法,例如 Executor,ExecutorService,ThreadPoolExecutor 都可以 ``` Executor executor = Executors.newFixedThreadPool(3); executor.execute(() -> { // 推送微信 }); executor.execute(() -> { // 推送邮箱 }); executor.execute(() -> { // 推送短信 }); ``` 不过请注意代码里面具体实现需要自己补充 ### 回答2: Java多线程可以实现一个同时推送微信、邮箱和短信接口。首先,我们可以创建一个推送任务类Task,该类实现了Runnable接口,在run方法实现推送微信、邮箱和短信的逻辑。 具体地,我们可以在Task类的构造函数传入推送的内容,并将微信、邮箱和短信推送逻辑在run方法实现。在run方法,我们可以使用多线程的方式分别推送微信、邮箱和短信。 为了实现多线程,我们可以使用Java的线程池来管理线程的创建和销毁。首先,我们可以创建一个线程池对象,设置线程池的大小为3,表示我们可以同时处理3个推送任务。然后,我们可以使用线程池的submit方法将任务提交给线程池执行。 当任务被提交给线程池后,线程池会自动创建一个线程来执行任务。由于我们设置了线程池的大小为3,所以最多同时会有3个线程来执行任务。当一个线程完成任务后,线程池会立即将其回收,以便提供给下一个任务使用。 通过使用线程池,我们可以实现并发执行多个推送任务的效果,提高推送的效率。同时,线程池会自动管理线程的创建和销毁,免去了手动创建和销毁线程的麻烦。这样,我们就可以实现一个同时推送微信、邮箱和短信接口

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReadThroughLife

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值