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

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

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

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

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

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

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

请求参数示例

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

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

名称 类型 示例值 描述
code String OK 请求状态码。返回OK代表请求成功。错误码见错误码列表
message String OK 状态码描述
requstId String F655A8D5B967 请求ID

返回参数示例

json 复制代码
{
  "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

java 复制代码
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

java 复制代码
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];
        }
    }
}

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

json 复制代码
{
	"code":30021,
	"message":"No Remain",
	"requestId":"cfcbiirc4v106cdb3mk0"
}

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

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

相关推荐
ac-er888811 分钟前
在Flask中处理后台任务
后端·python·flask
ac-er888818 分钟前
Flask中的钩子函数
后端·python·flask
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS在线文档管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Flying_Fish_roe1 小时前
Spring Boot-版本兼容性问题
java·spring boot·后端
摇滚侠2 小时前
spring cxf 常用注解
java·后端·spring
许野平2 小时前
Rust:Result 和 Error
开发语言·后端·rust·error·result
晓晨CH3 小时前
Spring考点总结
java·后端·spring
惜.己3 小时前
MyBatis中一对多关系的两种处理方法
java·开发语言·后端·sql·mysql·mybatis·idea
新知图书4 小时前
Django后台管理复杂模型
后端·python·django
尘浮生4 小时前
Java项目实战II基于Java+Spring Boot+MySQL的大学城水电管理系统(源码+数据库+文档)
java·开发语言·数据库·spring boot·后端·mysql·maven