🎉工作场景中遇到这样一个需求:在项目中接入天翼云短信推送接口,用于向用户推送短信消息,如短信验证码、系统信息推送以及推广信息等。
通过在天翼云短信服务官网进行一系列的调研之后,得到接入天翼云短信服务接口的基本步骤为:
- 注册账号,并进行实名认证。
- 开通短信服务:购买短信套餐。
- 创建短信内容:1、申请短信签名。2、申请短信模板。
- 调用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 协议请求,天翼云服务方都会根据访问中的签名信息验证访问请求者身份。因此,对于每次接口的调用请求,都需要由 accessKey
和 securityKey
进行请求签名验证实现之后才能实现。请求签名的具体步骤如下所示:
步骤一:获取 accessKey 和 securityKey:可以在天翼云官网--->个人中心--->基本信息中查看。
步骤二:构造时间戳:构造一个 eop-date 的时间戳,格式为 yyyymmddTHHMMSSZ, 简单来说就是"年月日T时分秒Z"。
步骤三:构造请求流水号:构造一个 ctyun-eop-request-id 的流水号,最好为每次请求不同,可以简单的使用 UUID.
步骤四:构造待签名字符串:
- 构造进行签名的Header :以
headerName:headerValue
来一个一个通过"\n"拼接起来,强制要求ctyun-eop-request-id
和eop-date
这个头作为header
中的一部分。将待签名算法的header
需要进行排序(headerName
以英文字母的顺序来排序),将排序后得到的列表进行遍历组装成待签名的header
. - 构造待签名的Query :Query 以 & 作为拼接,
key
和value
以"="连接,排序规则使用26个英文字母的顺序来排序,Query 参数全部都需要进行签名。 - 构造待签名的Body:传待发送的 Body 参数进行 sha256 摘要,对摘要出来的结果转十六进制。
- 将待签名的Header、Query、Body 通过"\n"进行连接。
步骤五:构造签名:
- 先将
securityKey
作为密钥,eop-date
作为数据,根据 hmacsha256 加密算法算出kTime
. - 将
kTime
作为密钥,accessKey
作为数据,根据 hmacsha256 加密算法算出kAk
. - 将
kAk
作为密钥,eop-date
的年月日值(前8位)作为数据,根据 hmacsha256 加密算法算出kDate
. - 将
kDate
作为密钥,步骤二的待签名字符串作为数据,根据 hmacsha256 加密算法算法签名并转化为 BASE64编码算出signature
.
步骤六:构造请求头:
- 将
eop-date
作为 key,步骤二的结果作为 value 加入 http 请求头中。 - 将
ctyun-eop-request-id
作为 key,步骤三的结果作为 value 加入 http 请求头中。 - 将
eop-Authorization
作为 key,通过字符串拼接的方式将accessKey
、Header
、signature
通过空格进行拼接,并将结果作为 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 也是天翼云短信接口那边返回的,可以在天翼云短信服务官网错误码列表 中查看到。
最后,需要注意的是,经过本人前期的一些调研,发现天翼云短信服务官方文档因为历史原因,相关信息未能及时更新,如发送短信接口的请求参数、返回参数示例中的字段大小写问题,应该统一首字母小写,而官网文档给出的是大写,可以及时联系天翼云的客服人员咨询。