本文介绍使用轻量消息队列(原 MNS)发送HTTP请求调用API时的请求结构、公共参数、返回结果及签名认证方式。
请求结构
服务地址
轻量消息队列(原 MNS)支持多个地域,每个地域分别提供了公网访问地址、内网访问地址。更多信息,请参见功能开服和接入点。
请求方法
轻量消息队列(原 MNS)支持通过HTTP协议进行请求通信。使用HTTP的PUT、POST、GET、DELETE等HTTP Method发送不同的请求。
发送的请求需要带上正确的请求参数、请求头和请求正文,请求及返回结果都使用UTF-8字符集进行编码。
公共参数
公共请求参数
参数 | 是否必选 | 说明 |
Authorization | 是 | 验证字符串。更多信息,请参见请求签名机制。 |
Content-Length | 是 | HTTP消息体的长度。 |
Content-Type | 是 | 请求内容的MIME类型,目前请求仅支持text或xml格式。 |
Content-MD5 | 否 | HTTP消息体的MD5值。更多信息,请参见The Content-MD5 Header Field。 |
Date | 是 | 请求的构造时间。目前只支持GMT格式,如果和SMQ的服务器时间前后差异超过15分钟将返回本次请求非法。 |
Host | 针对HTTP/1.1为必选,针对HTTP/1.0为可选。 | 从阿里云官网获取AccountId,从API文档中获取各地域SMQ访问地址,格式如下: |
x-mns-version | 是 | 调用SMQ接口的版本号。当前版本为2015-06-06。 |
x-mns-date | 否 | Date替代字段。用于解决部分浏览器上用户程序无法设置HTTP请求Date字段的场景。 |
公共返回参数
参数 | 说明 |
Content-Length | HTTP消息体返回的长度。 |
Connection | HTTP连接状态。 |
Date | 响应的返回时间,目前只支持GMT格式。 |
Server | 请求响应的SMQ服务器名。 |
x-mns-request-id | 此次Request操作的编号。 |
x-mns-version | SMQ接口的版本编号,当前版本是2015-06-06。 |
返回结果
调用API服务后返回数据采用统一格式。返回的HTTP状态码为2xx,说明调用成功;返回的HTTP状态码为4xx或5xx,说明调用失败。调用成功返回的数据格式为XML格式。
XML返回结果包括请求是否成功信息和具体的业务数据。示例如下:
<?xml version="1.0" encoding="utf-8"?>
<!--结果的根结点-->
<根节点 xmlns="http://mns.aliyuncs.com/doc/v1/">
<!--返回的子节点-->
</根节点>
调用接口出错后,将不会返回结果数据,HTTP请求返回一个4xx或5xx的HTTP状态码。返回的消息体中是具体的错误代码及错误信息。另外还包含一个全局唯一的请求ID:RequestId和一个您本次请求访问的站点ID:HostId。
具体的错误信息。请参见错误码。
请求签名机制
签名组成
轻量消息队列(原 MNS)服务会对每个访问的请求进行验证,每个请求向轻量消息队列(原 MNS)提交时,都需要在该请求的Header中包含签名(Authorization)。
轻量消息队列(原 MNS)通过使用AccessKeyId和AccessKeySecret进行对称加密的方法来验证请求的发送者身份。如果计算结果和提供的验证码一致,那么该请求有效;如果计算结果和提供的验证码不一致,那么轻量消息队列(原 MNS)将拒绝处理这次请求,并返回HTTP状态码403。
您需要在HTTP请求中增加Authorization的Head来包含签名信息,表明这个消息已被授权。格式为: Authorization: MNS AccessKeyId:Signature
。
您可以使用阿里云账号通过AccessKey管理页面申请和管理AccessKeyID和AccessKeySecret。AccessKeyId用于标识访问者的身份,AccessKeySecret是用于加密签名字符串和服务器端验证签名字符串的密钥,这两个参数必须严格保密。
计算方法
Signature = base64(hmac-sha1(HTTP_METHOD + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedMNSHeaders
+ CanonicalizedResource))
参数 | 描述 |
HTTP_METHOD | 大写的HTTP方法。例如:PUT、GET、POST、DELETE。 |
Content-Md5 | 请求内容数据的MD5值,如果请求的Header中没有传Content-MD5,则此处置空。 |
CONTENT-TYPE | 请求内容的类型。 |
DATE | 本次操作的时间。
|
CanonicalizedMNSHeaders | HTTP中的
|
CanonicalizedResource | HTTP所请求资源的URI。例如 |
用来签名的字符串为UTF-8格式。
签名的方法用RFC 2104中定义的HMAC-SHA1方法,其中Key为AccessKeySecret。
content-type和content-md5在请求中不是必须的,参数没有指定可以使用
''
来代替。
签名示例
请求示例如下:
PUT /queues/$queueName?metaOverride=true HTTP/1.1
Host: $AccountId.mns.cn-hangzhou.aliyuncs.com
Date: Wed, 08 Mar 2012 12:00:00 GMT
Authorization: MNS 15B4D3461F177624****:xQE0diMbL****f3YB+FIEXAMPLE=
<?xml version="1.0" encoding="UTF-8" ?>
<Queue xmlns="http://mns.aliyuncs.com/doc/v1/">
<VisibilityTimeout >60</VisibilityTimeout>
<MaximumMessageSize>1024</MaximumMessageSize>
<MessageRetentionPeriod>120</MessageRetentionPeriod>
<DelaySeconds>30</DelaySeconds>
</Queue>
返回示例如下:
示例一
如果传入的AccessKeyId不存在或disabled,返回403 Forbidden。
Content-Type: text/xml
Content-Length: 314
Date: Wed, 18Mar 2012 08:04:06 GMT
x-mns-request-id: 512B2A634403E52B1956****
<?xml version="1.0" encoding="utf-8"?>
<Error xmlns="http://mns.aliyuncs.com/doc/v1/">
<Code>AccessIDAuthError</Code>
<Message>
AccessID authentication fail, please check your AccessID and retry.
</Message>
<RequestId>512B2A634403E52B1956****</RequestId>
<HostId>mns.cn-hangzhou.aliyuncs.com</HostId>
</Error>
示例二
如果签名验证的时候,Header中没有传入Date或者格式不正确,返回403 Forbidden。
Content-Type: text/xml
Content-Length: 274
Date: Wed, 18Mar 2012 08:04:06 GMT
x-mns-request-id: 512B2A634403E52B1956****
<?xml version="1.0" encoding="UTF-8"?>
<Error xmlns="http://mns.aliyuncs.com/doc/v1/">
<Code>InvalidArgument</Code>
<Message>Date Header is invalid or missing.</Message>
<RequestId>7E1A5CF258F535884403****</RequestId>
<HostId>mns.cn-hangzhou.aliyuncs.com</HostId>
</Error>
示例三
传入请求的时间在轻量消息队列(原 MNS)服务器当前时间之后的15分钟以内,否则返回408超时。
Content-Type: text/xml
Content-Length: 283
Date: Wed, 11 May 2011 09:01:51 GMT
x-mns-request-id: 512B2A634403E52B1956****
<?xml version="1.0" encoding="UTF-8"?>
<Error xmlns="http://mns.aliyuncs.com/doc/v1/">
<Code>TimeExpired</Code>
<Message>
The http request you sent is expired.
</Message>
<RequestId>512B2A634403E52B1956****</RequestId>
<HostId>mns.cn-hangzhou.aliyuncs.com</HostId>
</Error>
Endpoint签名认证
在轻量消息队列(原 MNS)推送请求头中,Authorization字段的值是轻量消息队列(原 MNS)根据待签名字符串,用SHA1-RSA签名算法生成的签名。Endpoint可以使用公钥对签名进行验证,具体的验证方法如下:
步骤一:获取X509证书
在轻量消息队列(原 MNS)发送给Endpoint的HTTP请求头中,x-mns-signing-cert-url指定了签名证书的地址,您需要通过Base64解码,获取该签名文件URL地址,再从中提取出签名的公钥。
仅当签名证书的地址前缀为https://mnstest.oss-cn-hangzhou.aliyuncs.com/
时,该证书合法。否则,该证书不合法。更多信息,请参见如何确认轻量消息队列(原 MNS)推送请求中的公钥证书地址是阿里云官方的。
步骤二:计算待签名字符串
VERB + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedMNSHeaders
+ CanonicalizedResource
参数 | 示例 |
VERB | HTTP的方法 |
CONTENT-MD5 | 请求内容数据的MD5值。 |
CONTENT-TYPE | 请求内容的类型,对应的值为全小写。 |
DATE | 此次操作的时间,不能为空,目前只支持GMT格式。 |
CanonicalizedMNSHeaders | HTTP请求头中的x-mns-开头的字段组合。 |
CanonicalizedResource | HTTP请求的相对地址,不能为空。 |
待签名字符串示例:
POST
ZDgxNjY5ZjFlMDQ5MGM0YWMwMWE5ODlmZDVlYmQxYjI=
text/xml;charset=utf-8
Wed, 25 May 2016 10:46:14 GMT
x-mns-request-id:57458276F0E3D56D7C00****
x-mns-signing-cert-url:aHR0cDovL21uc3Rlc3Qub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS94NTA5X3B1YmxpY19jZXJ0aWZpY2F0ZS5w****
x-mns-version:2015-06-06
/notifications
步骤三:Authorization解密
对Authorization签名字段进行Base64解码后,使用从步骤一中提取的公钥对其进行解密。
步骤四:认证
比较步骤二生成的待签名字符串与步骤三解密的结果是否一致。如果一致,说明请求来自轻量消息队列(原 MNS),访问合法。
CanonicalizedMNSHeaders在签名验证前需要符合以下规范:
Head的名字需要变成小写。
Head自小到大排序。
分割Headname和value的冒号前后不能有空格。
每个Head之后都有一个\n,如果没有以x-mns-开头的Head,则在签名时CanonicalizedMNSHeaders就设置为空。
其他认证说明:
用来签名的字符串为UTF-8格式。
签名的方法用RFC 3447中定义的sha1WithRSAEncryption方法。
Base64是指使用Base64算法转码文本。
Java示例代码
public class SignDemo {
private Boolean authenticate(String method, String uri, Map<String, String> headers) {
try {
//获取证书的URL。
if (!headers.containsKey("x-mns-signing-cert-url")) {
System.out.println("x-mns-signing-cert-url Header not found");
return false;
}
String cert = headers.get("x-mns-signing-cert-url");
if (cert.isEmpty()) {
System.out.println("x-mns-signing-cert-url empty");
return false;
}
cert = new String(Base64.decodeBase64(cert));
System.out.println("x-mns-signing-cert-url:\t" + cert);
//根据URL获取证书,并从证书中获取公钥。
URL url = new URL(cert);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
DataInputStream in = new DataInputStream(conn.getInputStream());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate c = cf.generateCertificate(in);
PublicKey pk = c.getPublicKey();
//获取待签名字符串。
String str2sign = getSignStr(method, uri, headers);
System.out.println("String2Sign:\t" + str2sign);
//对Authorization字段做Base64解码。
String signature = headers.get("Authorization");
byte[] decodedSign = Base64.decodeBase64(signature);
//认证。
java.security.Signature signetcheck = java.security.Signature.getInstance("SHA1withRSA");
signetcheck.initVerify(pk);
signetcheck.update(str2sign.getBytes());
Boolean res = signetcheck.verify(decodedSign);
return res;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private String getSignStr(String method, String uri, Map<String, String> headers) {
StringBuilder sb = new StringBuilder();
sb.append(method);
sb.append("\n");
sb.append(safeGetHeader(headers, "Content-md5"));
sb.append("\n");
sb.append(safeGetHeader(headers, "Content-Type"));
sb.append("\n");
sb.append(safeGetHeader(headers, "Date"));
sb.append("\n");
List<String> tmp = new ArrayList<String>();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().startsWith("x-mns-")) {
tmp.add(entry.getKey() + ":" + entry.getValue());
}
}
Collections.sort(tmp);
for (String kv : tmp) {
sb.append(kv);
sb.append("\n");
}
sb.append(uri);
return sb.toString();
}
private String safeGetHeader(Map<String, String> headers, String name) {
if (headers.containsKey(name)) {
return headers.get(name);
} else {
return "";
}
}
public static void main(String[] args) {
SignDemo sd = new SignDemo();
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "Mko2Azg9fhCw8qR6G7AeAFMyzjO9qn7LDA5/t9E+6X5XURXTqBUuhpK+K55UNhrnlE2UdDkRrwDxsaDP5ajQ****");
headers.put("Content-md5", "M2ViOTE2ZDEyOTlkODBjMjVkNzM4YjNhNWI3ZWQ1****");
headers.put("Content-Type", "text/xml;charset=utf-8");
headers.put("Date", "Tue, 23 Feb 2016 09:41:06 GMT");
headers.put("x-mns-request-id", "56CC2932F0E3D5BD5306****");
headers.put("x-mns-signing-cert-url", "aHR0cDovL21uc3Rlc3Qub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS94NTA5X3B1YmxpY19jZXJ0aWZpY2F0ZS5w****");
headers.put("x-mns-version", "2015-06-06");
Boolean res = sd.authenticate("POST", "/notifications", headers);
System.out.println("Authenticate result:" + res);
}
}