基于浏览器上传的表单中携带签名
OBS服务支持基于浏览器的POST上传对象请求,此类请求的签名信息通过表单的方式上传。POST上传对象:首先,创建一个安全策略,指定请求中需要满足的条件,比如:桶名、对象名前缀;然后,创建一个基于此策略的签名,需要签名的请求表单中必须包含有效的signature和policy;最后,创建一个表单将对象上传到桶中。
签名的计算过程如下:
- 对policy内容进行UTF-8编码。
- 对第一步的结果进行Base64编码。
- 使用SK对第二步的结果进行HMAC-SHA1签名计算。
- 对第三步的结果进行Base64编码,得到签名。
StringToSign = Base64( UTF-8-Encoding-Of( policy ) ) Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, StringToSign ) )
{ "expiration": "2017-12-31T12:00:00.000Z",
"conditions": [
{"x-obs-acl": "public-read" },
{"x-obs-security-token": "YwkaRTbdY8g7q...." },
{"bucket": "book" },
["starts-with", "$key", "user/"]
]
}
Policy策略中包含有效时间Expiration和条件元素Conditions。
Expiration
描述本次签名的有效时间ISO 8601 UTC,如实例中"expiration": "2017-12-31T12:00:00.000Z"表示请求在2017年12月31日12点之后无效。该字段是policy中必选字段。合法格式仅有"yyyy-MM-dd'T'HH:mm:ss'Z'"和"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"。
Conditions
Conditions是一个用于验证本次请求合法的一种机制,可以使用这些条件限制请求中必须包含的内容。实例中的条件要求请求的桶名必须是book,对象名必须以user/为前缀,对象的acl必须是公共可读。除了AccessKeyId、signature、file、policy、token、field names以及前缀为x-ignore-外的表单中的所有项,都需要包含在policy中。下表是conditions中应该包含的项:
元素名称 |
描述 |
---|---|
x-obs-acl |
请求中的ACL。 支持精确匹配和starts-with条件匹配。 |
content-length-range |
设置上传对象的最大最小长度,支持range匹配。 |
Cache-Control, Content-Type, Content-Disposition, Content-Encoding, Expires |
REST请求特定头域。 支持精确匹配和starts-with条件匹配。 |
key |
上传对象的名字。 支持精确匹配和starts-with条件匹配。 |
bucket |
请求桶名。 支持精确匹配。 |
success_action_redirect |
上传对象成功后重定向的URL地址。具体描述请参见5.4.2-POST上传。 支持精确匹配和starts-with条件匹配。 |
success_action_status |
如果未指定success_action_redirect,则成功上传时返回给客户端的状态码。具体描述请参见5.4.2-POST上传。 支持精确匹配。 |
x-obs-meta-* |
用户自定义元数据。 元素中的关键字不允许含有非ASCII码或不可识别字符,如果一定要使用非ASCII码或不可识别字符,需要客户端自行做编解码处理,可以采用URL编码或者Base64编码,服务端不会做解码处理。 支持精确匹配和starts-with条件匹配。 |
x-obs-* |
其他以x-obs-为前缀的头域。 支持精确匹配和starts-with条件匹配。 |
x-obs-security-token |
请求消息头中字段名。 临时AK/SK和securitytoken鉴权必加字段名。 |
Policy条件匹配的方式如下:
条件 |
描述 |
---|---|
Exact Matches |
默认是完全匹配,post表单中该项的值必须和policy的conditions中设置的值完全一样。例如:上传对象的同时设置对象ACL为public-read,表单中x-obs-acl元素的值为public-read,policy中的conditions可以设置为 {"x-obs-acl": "public-read" }或者[ "eq", "$x-obs-acl", "public-read"],这两者是等效的。 |
Starts With |
如果使用该条件,则post表单中对应元素的值必须是固定字符串开始。例如:上传对象名以user/为前缀,表单中key元素的值可以是user/test1、user/test2,policy的conditions中该条件如下: ["starts-with", "$key", "user/"] |
Matching Any Content |
post表单中对应元素的值可以是任意值。例如:请求成功后重定向的地址可以是任意地址,表单中success_action_redirect元素的值可以是任意值,policy的conditions中该条件如下: ["starts-with", "$success_action_redirect", ""] |
Specifying Ranges |
post表单中file元素文件的内容长度可以是一个指定的范围,只用于限制对象大小。例如上传对象大小为1-10MB,表单中file元素的内容长度可以是1048576-10485760,policy的conditions中该条件如下,注意值没有双引号: ["content-length-range", 1048576, 10485760] |
policy使用json格式,conditions可以支持 { } 和 [ ] 两种方式,{ }中包含表单元素的key和value两项,以冒号分隔;[ ]中包含条件类型、key、value三项,以逗号分隔,元素key之前使用$字符表示变量。
Policy中必须转义的字符如下:
转义后的字符 |
真实字符 |
---|---|
\\ |
反斜杠(\) |
\$ |
美元符号($) |
\b |
退格 |
\f |
换页 |
\n |
换行 |
\r |
回车 |
\t |
水平制表 |
\v |
垂直制表 |
\uxxxx |
所有Unicode字符 |
请求和Policy示例
下面的几张表提供了一些请求和Policy的例子。
示例1:在examplebucket桶中上传testfile.txt对象,并且设置对象ACL为公共可读。
请求 |
policy |
---|---|
POST / HTTP/1.1 Host: examplebucket.obs.cn-north-4.myhuaweicloud.com Content-Type: multipart/form-data; boundary=7e32233530b26 Content-Length: 1250 --7e32233530b26 Content-Disposition: form-data; name="key" testfile.txt --7e32233530b26 Content-Disposition: form-data; name="x-obs-acl" public-read --7e32233530b26 Content-Disposition: form-data; name="content-type" text/plain --7e32233530b26 Content-Disposition: form-data; name="AccessKeyId" UDSIAMSTUBTEST000002 --7e32233530b26 Content-Disposition: form-data; name="policy" ewogICJleHBpcmF0aW9uIjogIjIwMTktMDctMDFUMTI6MDA6MDAuMDAwWiIsCiAgImNvbmRpdGlvbnMiOiBbCiAgICB7ImJ1Y2tldCI6ICJleGFtcGxlYnVja2V0IiB9LAogICAgWyJlcSIsICIka2V5IiwgInRlc3RmaWxlLnR4dCJdLAoJeyJ4LW9icy1hY2wiOiAicHVibGljLXJlYWQiIH0sCiAgICBbImVxIiwgIiRDb250ZW50LVR5cGUiLCAidGV4dC9wbGFpbiJdLAogICAgWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsIDYsIDEwXQogIF0KfQo= --7e32233530b26 Content-Disposition: form-data; name="signature" xxl7bZs/5FgtBUggOdQ88DPZUo0= --7e32233530b26 Content-Disposition: form-data; name="file"; filename="E:\TEST_FILE\TEST.txt" Content-Type: text/plain 123456 --7e32233530b26 Content-Disposition: form-data; name="submit" Upload --7e32233530b26-- |
{ "expiration": "2019-07-01T12:00:00.000Z", "conditions": [ {"bucket": "examplebucket" }, ["eq", "$key", "testfile.txt"], {"x-obs-acl": "public-read" }, ["eq", "$Content-Type", "text/plain"] ] } |
示例2:在examplebucket桶中上传file/obj1对象,并且设置对象的四个自定义元数据。
请求 |
policy |
---|---|
POST / HTTP/1.1 Host: examplebucket.obs.cn-north-4.myhuaweicloud.com Content-Type: multipart/form-data; boundary=7e329d630b26 Content-Length: 1597 --7e3542930b26 Content-Disposition: form-data; name="key" file/obj1 --7e3542930b26 Content-Disposition: form-data; name="AccessKeyId" UDSIAMSTUBTEST000002 --7e3542930b26 Content-Disposition: form-data; name="policy" ewogICJleHBpcmF0aW9uIjogIjIwMTktMDctMDFUMTI6MDA6MDAuMDAwWiIsCiAgImNvbmRpdGlvbnMiOiBbCiAgICB7ImJ1Y2tldCI6ICJleGFtcGxlYnVja2V0IiB9LAogICAgWyJzdGFydHMtd2l0aCIsICIka2V5IiwgImZpbGUvIl0sCiAgICB7Ingtb2JzLW1ldGEtdGVzdDEiOiJ2YWx1ZTEifSwKICAgIFsiZXEiLCAiJHgtb2JzLW1ldGEtdGVzdDIiLCAidmFsdWUyIl0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiR4LW9icy1tZXRhLXRlc3QzIiwgImRvYyJdLAogICAgWyJzdGFydHMtd2l0aCIsICIkeC1vYnMtbWV0YS10ZXN0NCIsICIiXQogIF0KfQo= --7e3542930b26 Content-Disposition: form-data; name="signature" HTId8hcaisn6FfdWKqSJP9RN4Oo= --7e3542930b26 Content-Disposition: form-data; name="x-obs-meta-test1" value1 --7e3542930b26 Content-Disposition: form-data; name="x-obs-meta-test2" value2 --7e3542930b26 Content-Disposition: form-data; name="x-obs-meta-test3" doc123 --7e3542930b26 Content-Disposition: form-data; name="x-obs-meta-test4" my --7e3542930b26 Content-Disposition: form-data; name="file"; filename="E:\TEST_FILE\TEST.txt" Content-Type: text/plain 123456 --7e3542930b26 Content-Disposition: form-data; name="submit" Upload --7e3542930b26-- |
{ "expiration": "2019-07-01T12:00:00.000Z", "conditions": [ {"bucket": "examplebucket" }, ["starts-with", "$key", "file/"], {"x-obs-meta-test1":"value1"}, ["eq", "$x-obs-meta-test2", "value2"], ["starts-with", "$x-obs-meta-test3", "doc"], ["starts-with", "$x-obs-meta-test4", ""] ] } |
Java中签名的计算方法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.TimeZone; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class SignDemo { private static final String DEFAULT_ENCODING = "UTF-8"; private static final String EXPIRATION_DATE_FORMATTER = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; private static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT"); private static final long DEFAULT_EXPIRE_SECONDS = 300; private String ak; private String sk; private String join(List<?> items) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < items.size(); i++) { String item = items.get(i).toString(); sb.append(item); if (i < items.size() - 1) { sb.append(","); } } return sb.toString(); } private String stringToSign(String[] tmpConditions, String expiration) { List<String> conditions = new ArrayList<>(); Collections.addAll(conditions, tmpConditions); return "{\"expiration\":" + "\"" + expiration + "\"," + "\"conditions\":[" + join(conditions) + "]}"; } private String getFormatExpiration(Date requestDate, long expires) { requestDate = requestDate != null ? requestDate : new Date(); SimpleDateFormat expirationDateFormat = new SimpleDateFormat(EXPIRATION_DATE_FORMATTER); expirationDateFormat.setTimeZone(GMT_TIMEZONE); Date expiryDate = new Date(requestDate.getTime() + (expires <= 0 ? DEFAULT_EXPIRE_SECONDS : expires) * 1000); return expirationDateFormat.format(expiryDate); } public String postSignature(String policy) throws Exception { byte[] policyBase64 = Base64.getEncoder().encode(policy.getBytes(DEFAULT_ENCODING)); SecretKeySpec signingKey = new SecretKeySpec(this.sk.getBytes(DEFAULT_ENCODING), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return Base64.getEncoder().encodeToString(mac.doFinal(policyBase64)); } public static void main(String[] args) throws Exception { SignDemo demo = new SignDemo(); /* 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全; 本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。*/ demo.ak = System.getenv("HUAWEICLOUD_SDK_AK"); demo.sk = System.getenv("HUAWEICLOUD_SDK_SK"); String authExpiration = demo.getFormatExpiration(null, 0); String[] tmpConditions = { "{\"bucket\": \"bucketName\" }", "[\"starts-with\", \"$key\", \"obj\"]" }; String policy = demo.stringToSign(tmpConditions, authExpiration); String policyBase64 = Base64.getEncoder().encodeToString(policy.getBytes(DEFAULT_ENCODING)); String signature = demo.postSignature(policy); // 表单中携带AccessKeyId、policy、signature的签名 System.out.println("authExpiration=" + authExpiration); System.out.println("policy=" + policy); System.out.println("policyBase64=" + policyBase64); System.out.println("signature=" + signature); // 表单中携带token的签名 System.out.println("token=" + demo.ak + ":" + signature + ":" + policyBase64); } } |
Python中签名的计算方法为:
# coding=utf-8 import binascii import hashlib import hmac import os import time from datetime import datetime import pytz class SignatureDemo: EXPIRATION_DATE_FORMATTER = "%Y-%m-%dT%H:%M:%S.%f" DEFAULT_ENCODING = "UTF-8" # 默认过期时间五分钟 DEFAULT_EXPIRE_SECONDS = 300 GMT_TIMEZONE = "GMT" def __init__(self, ak=None, sk=None): self.ak = ak self.sk = sk # request_date和expires是timestamp形式 e.g. 1675651495.979 def get_format_expiration(self, request_date, expires): request_date = request_date if request_date else time.time() expiry_date = request_date + (expires if expires > 0 else self.DEFAULT_EXPIRE_SECONDS) expiration = datetime.fromtimestamp(expiry_date, pytz.timezone(self.GMT_TIMEZONE)).strftime( self.EXPIRATION_DATE_FORMATTER)[:-3] + "Z" return expiration def post_signature(self, policy): # 如果使用binascii或encode("base64"), 需要去除换行符 policy_base64 = binascii.b2a_base64(policy.encode(self.DEFAULT_ENCODING)).rstrip() hashed = hmac.new(self.sk.encode(self.DEFAULT_ENCODING), policy_base64, hashlib.sha1) return binascii.b2a_base64(hashed.digest()).rstrip() @staticmethod def string_to_sign(conditions, expiration): return "{\"expiration\":" + "\"" + expiration + "\"," + "\"conditions\":[" + ",".join(conditions) + "]}" if __name__ == "__main__": demo = SignatureDemo() # 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全; # 本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。 demo.ak = os.getenv('HUAWEICLOUD_SDK_AK') demo.sk = os.getenv('HUAWEICLOUD_SDK_SK') auth_expiration = demo.get_format_expiration(None, 0) conditions_example = [ "{\"bucket\": \"bucketName\" }", "[\"starts-with\", \"$key\", \"obj\"]" ] post_policy = demo.string_to_sign(conditions_example, auth_expiration) policy_base64 = binascii.b2a_base64(post_policy.encode(demo.DEFAULT_ENCODING)).rstrip() signature = demo.post_signature(post_policy) # 表单中携带AccessKeyId、policy、signature的签名 print("authExpiration=" + auth_expiration) print("policy=" + post_policy) print("policyBase64=" + policy_base64) print("signature=" + signature) # 表单中携带token的签名 print("token=" + demo.ak + ":" + signature + ":" + policy_base64)