用户签名验证

美团对象存储服务 (MSS, Meituan Storage Service) 提供 S3 兼容的API访问接口,以方便用户使用对象存储。 为了访问 MSS 开放接口,MSS 为每个用户分配访问的令牌和密码( AccessKey 和 AccessSecret ),每个访问请求都需要携带 AccessKey 以及使用AccessSecret 对请求数据的数字签名。用户可以通过MSS控制台管理界面的“API密钥”界面,查询访问API的 AccessKey 以及 AccessSecret。

MSS RESTful API 与 Amazon S3 兼容,使用标准的 HTTP Authorization 标头来传递身份验证信息。其格式如下:

Authorization: AWS AccessKey:Signature

Signature 是请求中选定元素拼接的字符串。

当用户想以个人身份向 MSS 发送请求时,需要首先将发送的请求按照MSS指定的格式生成签名字符串;然后使用 AccessSecret 对签名字符串进行加密产生验证码。 MSS 收到请求以后,会通过 AccessKey 找到对应的 AccessSecret,以同样的方法提取签名字符串和验证码,如果计算出来的验证码和提供的一样即认为该请求是有效的;否则,MSS将拒绝处理这次请求,并返回 HTTP 403 错误。

在URL中包含签名

默认情况下,MSS 的 Bucket 和 Object 是私有的,预签名授权访问通过在URL中加入签名信息,实现第三方用户的授权访问。预签名 URL 仅在指定的持续时间内有效。

实现方式

URL签名示例:

http://mtmss.com/mss-test-bucket/?acl&AWSAccessKeyId=7f23221b13874555a9eadcef8a761bb&Expires=1511604364&Signature=mskGXDpl1wjusQRCJaA500nbc1w%3D

预签名 URL 中至少包含Signature,Expires,AWSAccessKeyId三个参数,参数定义如下:

名称 示例值 描述
AWSAccessKeyId 7f23221b13874555a9eadcef8a761bb 用户的 AccessKey。
Expires 1511604364 这个参数的值是一个 UNIX 时间(自 UTC 时间1970年1月1号开始的秒数),用于标识该 URL 的超时时间。
Signature mskGXDpl1wjusQRCJaA500nbc1w%3D 表示签名信息。所有的 MSS 支持的请求和各种 Header 参数,在 URL 中进行签名的算法和在 Header 中包含签名的算法稍有差别。将签名字符串放到 URL 时,注意要对 URL 进行urlencode。
http://MSSHost/BucketName/ObjectName?AWSAccessKeyId=AWSAccessKeyId&Expires=Expires&Signature=Signature

Signature = Base64( HMAC-SHA1( AccessSecret, UTF-8-Encoding-Of( StringToSign ) ) );
StringToSign = HTTP-Verb + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    Expires + "\n" +
    CanonicalizedAmzHeaders +
    CanonicalizedResource;

示例代码

URL中添加签名的python示例代码:

#!/usr/bin/env python2.7
# encoding: utf-8

import base64
import hmac
import sha
import urllib
import datetime
import requests
import time

AccessKey = "7f23221b13874555a9eadcef8a761bb"
AccessSecret = "f1fa4e8370962e4a79dd865f61a3f8e"
GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
Date = datetime.datetime.utcnow().strftime(GMT_FORMAT)
Expires = str(int(time.time()) + 60)
AWSAccessKeyId = AccessKey

StringToSign = ("PUT" + "\n" +
                "" + "\n" +
                "" + "\n" +
                Expires + "\n" +
                "x-amz-acl:public-read" + "\n" +
                "/mss-test-bucket/?acl")
h = hmac.new(AccessSecret,
             StringToSign,
             sha)
Signature = urllib.quote(base64.encodestring(h.digest()).strip())
url = "http://mtmss.com/mss-test-bucket/?acl&AWSAccessKeyId=%s&Expires=%s&Signature=%s" % (AWSAccessKeyId,
                                                                                       Expires,
                                                                                       Signature)
headers = {
    "Date": Date,
    "Host": "mtmss.com",
    "x-amz-acl": "public-read",
}
r = requests.request("PUT", url, headers=headers)
print r.text
print r.status_code

细节分析

URL中进行签名的算法与header中包含签名相比主要区别如下:

  1. 通过 URL 包含签名时,之前的 Date 参数换成 Expires 参数。
  2. 不支持同时在 URL 和 Head 中包含签名。
  3. 将签名字符串放到 URL 时,注意要对 URL 进行 urlencode。
  4. 使用在 URL 中签名的方式,会将你授权的数据在过期时间以内曝露在互联网上,请预先评估使用风险。
  5. 在 URL 中添加签名时,Signature,Expires,AWSAccessKeyId 顺序可以交换,但是如果 Signature,Expires,AWSAccessKeyId 缺少其中的一个或者多个,返回 403 Forbidden。错误码:AccessDenied。
  6. 如果访问的当前时间晚于请求中设定的 Expires 时间,返回 403 Forbidden。错误码:AccessDenied。
  7. 如果 Expires 时间格式错误,返回 403 Forbidden。错误码:AccessDenied。
  8. 生成签名字符串时,除 Date 被替换成 Expires 参数外,仍然包含 content-type、content-md5 等上节中定义的 Header。(请求中虽然仍然有 Date 这个请求头,但不需要将 Date 加入签名字符串中)

在Header中包含签名

用户可以在 HTTP 请求中增加 Authorization 的 Header 来包含签名(Signature)信息,表明这个消息已被授权。

Authorization字段计算的方法

Authorization = "AWS" + " " + AccessKey + ":" + Signature;
Signature = Base64( HMAC-SHA1( AccessSecret, UTF-8-Encoding-Of( StringToSign ) ) );

StringToSign = HTTP-Verb + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    Date + "\n" +
    CanonicalizedAmzHeaders +
    CanonicalizedResource;
  1. HTTP-Verb 表示 HTTP 请求的 Method,主要有 PUT,GET,POST,HEAD,DELETE 等。
  2. Content-MD5 表示请求内容数据的 MD5 值,对消息内容(不包括头部)计算 MD5 值获得128比特位数字,对该数字进行 base64 编码而得到。该请求头可用于消息合法性的检查(消息内容是否与发送时一致),如"6M23UrePhW4UO6IWrR6lCw==",也可以为空。详情参看 RFC2616 Content-MD5
  3. Content-Type 表示请求内容的类型,如”text/html”,也可以为空。
  4. Date 表示此次操作的时间,且必须为 GMT 格式,如”Fri, 24 Nov 2017 07:53:57 GMT”。
  5. CanonicalizedAmzHeaders 表示以 x-amz- 为前缀的 http header 的字典序排列。
  6. CanonicalizedResource 表示用户想要访问的 MSS 资源。 其中,Date 和 CanonicalizedResource 不能为空;如果请求中的 Date 时间和 MSS 服务器的时间差15分钟以上,MSS 服务器将拒绝该服务,并返回 HTTP 403 错误。 Date 也可以使用 x-amz-date 替换。

构建CanonicalizedMSSHeaders的方法

所有以 x-amz- 为前缀 的HTTP Header 被称为 CanonicalizedAmzHeaders(使用不区分大小写的对比方式)。它的构建方法如下:

  • 将所有以 x-amz- 为前缀的 HTTP 请求头的名字转换成小写。例如 X-Amz-Company: Meituan 转换为 x-amz-company: Meituan
  • 把所有以 x-amz- 为前缀的 HTTP 请求头按照字典顺序进行排序。
  • 将多个相同标头都以 x-amz- 为前缀的合并成一个,用逗号进行分割,之间不能留空格。例如 x-amz-company:Meituanx-amz-company:Dianping 合并为 x-amz-company:Meituan,Dianping
  • 删除 HTTP 请求头和内容之间分隔符两端出现的任何空格。例如 x-amz-company: Meituan 转换为 x-amz-company:Meituan
  • 在每一个 HTTP 请求头的内容后添加一个换行符 (\n) ,从而构建出 CanonicalizedAmzHeaders。
  • 注意:
  • CanonicalizedAmzHeaders 可以为空,当为空时不需要添加换行符 (\n)
  • 当只有一个时,则如 x-amz-company:Meituan\n ,注意最后的 (\n)
  • 当含有多个时,则如 x-amz-company:Meituan\nx-amz-localtion:Beijing\n ,注意最后的 (\n)

构建CanonicalizedResource的方法

用户发送请求中想访问的MSS目标资源被称为CanonicalizedResource。CanonicalizedResource由HTTP请求路径和HTTP请求参数二部分构成。它的构建方法如下:

  1. 将CanonicalizedResource置成空字符串 ""
  2. 放入要访问的 MSS 资源 /BucketName/ObjectName无ObjectName则CanonicalizedResource为”/BucketName/“,如果同时也没有BucketName则为“/”)。
  3. 如果请求的资源包括子资源(SubResource) ,那么将所有的子资源按照字典序,从小到大排列并以 & 为分隔符生成子资源字符串。在 CanonicalizedResource 字符串尾添加 ?\ 和子资源字符串。此时的 CanonicalizedResource 如: /BucketName/ObjectName?acl&uploadId=UploadId
  • 提示:
  • MSS目前支持的子资源(sub-resource)包括:acl,uploads,location,cors,logging,website,lifecycle,delete,uploadId,partNumber,response-content-type,response-content-language,response-expires,response-cache-control,response-content-disposition,response-content-encoding,domain,notification,policy,requestPayment,torrent,versionId,versioning,versions

计算签名头规则

  1. 签名的字符串必须为 UTF-8 格式。含有中文字符的签名字符串必须先进行 UTF-8 编码,再与 AccessSecret 计算最终签名。
  2. Content-TypeContent-MD5 在请求中不是必须的,但是如果请求需要签名验证,空值的话以换行符 \n 代替。
  3. 在所有 非HTTP 标准定义的 header 中,只有以 x-amz- 开头的 header,需要加入签名字符串;其他非 HTTP 标准 header 将被 MSS 忽略。

签名示例

假如 AccessKey 是”7f23221b13874555a9eadcef8a761bb”,AccessSecret 是”f1fa4e8370962e4a79dd865f61a3f8e”

请求 签名字符串计算公式 签名字符串
PUT /mss-test-bucket/?acl HTTP/1.1
Accept: /
Accept-Encoding: gzip, deflate
Authorization: AWS 825a6cb3cbfb438b89e28fb66a9
44e19:F0qsjrfma6v7nrO2vV2uGpnpqZQ=
Connection: keep-alive
Date: Thu, 09 Nov 2017 05:19:18 GMT
Host: mtmss.com
x-amz-acl: public-read
Signature = Base64(
HMAC-SHA1( AccessSecret, UTF-8-Encoding-Of( StringToSign ) ) );
StringToSign = HTTP-Verb + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
Date + "\n" +
CanonicalizedAmzHeaders +
CanonicalizedResource;
StringToSign =
"PUT" + "\n" +
"" + "\n" +
"" + "\n" +
"Thu, 09 Nov 2017 05:19:18 GMT" + "\n" +
"x-amz-acl:public-read" + "\n" +
"/mss-test-bucket/?acl"

可用以下方法计算签名 (Signature) 与 Authorization,并使用 Python2.7 发送 HTTP 请求到 MSS

#!/usr/bin/env python2.7
# encoding: utf-8
import base64
import hmac
import sha
import datetime
import requests

AccessKey = "7f23221b13874555a9eadcef8a761bb"
AccessSecret = "f1fa4e8370962e4a79dd865f61a3f8e"
GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
Date = datetime.datetime.utcnow().strftime(GMT_FORMAT)
StringToSign = ("PUT" + "\n" +
                "" + "\n" +
                "" + "\n" +
                Date + "\n" +
                "x-amz-acl:public-read" + "\n" +
                "/mss-test-bucket/?acl")
h = hmac.new(AccessSecret,
             StringToSign,
             sha)
Signature = base64.b64encode(h.digest())
url = "http://mtmss.com/mss-test-bucket/?acl"
headers = {
    "Authorization": "AWS" + " " + AccessKey + ":" + Signature,
    "Date": Date,
    "Host": "mtmss.com",
    "x-amz-acl": "public-read",
}

r = requests.request("PUT", url, headers=headers)
print r.text
print r.status_code

细节分析

  1. 如果传入的 AccessKeyId 不存在,返回 403 Forbidden。
  2. 经身份验证的请求随附的客户端时间戳必须处于收到请求时的 MSS 系统时间的 15 分钟之内,否则返回 403 Forbidden。错误码:RequestTimeTooSkewed。
  3. 如果您在标准化标头中包含“Date”标头的值时遇到困难,您可以改用“x-amz-date”标头为请求设置时间戳。x-amz-date 标头的值必须采用 RFC 2616 格式中的任意格式。
  4. x-amz-date 标头位于请求中时,系统将在计算请求签名时忽略任何 Date 标头。因此,如果包含了 x-amz-date 标头,请在构建 StringToSign 时使用 Date 的空字符串。

常见问题

Content-MD5的计算方法

  1. 先计算 MD5 加密的二进制数组(128位)。
  2. 再对这个二进制进行 base64 编码(而不是对32位字符串编码)。
import base64,hashlib
hash = hashlib.md5()
hash.update("MSS, Meituan Storage Service")
content_md5 = base64.b64encode(hash.digest())