文档

请求协议说明

更新时间:

本文介绍使用消息服务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格式,如果和MNS的服务器时间前后差异超过15分钟将返回本次请求非法。

Host

针对HTTP/1.1为必选,针对HTTP/1.0为可选。

从阿里云官网获取AccountId,从API文档中获取各地域MNS访问地址,格式如下:$AccountId.mns.cn-hangzhou.aliyuncs.com

x-mns-version

调用MNS接口的版本号。当前版本为2015-06-06。

x-mns-date

Date替代字段。用于解决部分浏览器上用户程序无法设置HTTP请求Date字段的场景。

公共返回参数

参数

说明

Content-Length

HTTP消息体返回的长度。

Connection

HTTP连接状态。

Date

响应的返回时间,目前只支持GMT格式。

Server

请求响应的MNS服务器名。

x-mns-request-id

此次Request操作的编号。

x-mns-version

MNS接口的版本编号,当前版本是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是用于加密签名字符串和服务器端验证签名字符串的密钥,这两个参数必须严格保密。

计算方法

Authorization = 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

本次操作的时间。

  • 格式为:Thu, 07 Mar 2012 18:49:58 GMT。如果用x-mns-date替代DATE,则DATE不能填空,需用x-mns-date的值替换。

  • 此参数不能为空,目前只支持GMT格式。

  • 如果请求时间和消息服务MNS服务器时间相差超过15分钟,消息服务MNS会判定此请求不合法,返回错误码400。更多错误信息及错误码,请参见错误码

CanonicalizedMNSHeaders

HTTP中的x-mns-开头的字段组合。该字段在签名验证前需要符合以下规范:

  • head的名字需要变成小写。

  • head自小到大排序。

  • 分割head name和value的冒号前后不能有空格。

  • 每个Head之后都有一个\n,如果没有以x-mns-开头的head,则在签名时CanonicalizedMNSHeaders就设置为空。

CanonicalizedResource

HTTP所请求资源的URI。例如/queues/$queueName?metaOverride=true

说明
  • 用来签名的字符串为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);
    }
}