文档首页/ 对象存储服务 OBS/ API参考/ 如何调用API/ 认证鉴权/ 基于浏览器上传的表单中携带签名
更新时间:2024-09-29 GMT+08:00

基于浏览器上传的表单中携带签名

功能介绍

OBS服务支持基于浏览器的POST上传对象请求,此类请求的签名信息通过表单的方式上传。计算POST表单上传请求的签名,需要先定义一个安全策略(policy),这个安全策略的作用是限制表单上传的内容,例如规定表单上传对象的对象名前缀必须以“prefix01”开头,使用policy能够帮助您更好的管控桶中的文件。POST上传对象的流程是:

  1. 创建一个policy,指定请求中需要满足的条件,比如:桶名、对象名前缀。
  2. 计算基于此policy的签名。
  3. 创建一个表单,表单中必须包含有效的签名和policy,使用该表单将对象上传到桶中。

步骤一:创建policy

下面将以如下policy为例介绍policy的组成和语法。下方的policy限制了签名的有效时间为2024年12月31日12点之后无效,上传对象的访问权限限制为公共读,请求消息头中的x-obs-security-token须等于"YwkaRTbdY8g7q...." },对象上传的桶名需要为“book”,对象名必须以前缀“user/”开头:

1
2
3
4
5
6
7
8
{ "expiration": "2024-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

表1 有效时间Expiration

参数名称

参数类型

是否必选

描述

Expiration

String

必选

参数解释:

描述本次签名的有效时间。如示例中"expiration": "2024-12-31T12:00:00.000Z"表示请求在2024年12月31日12点之后无效。

约束限制:

格式为ISO 8601 UTC,"yyyy-MM-dd'T'HH:mm:ss'Z'"或"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"。

取值范围:

不涉及

默认取值:

条件元素:Conditions

使用Conditions可以要求调用请求必须满足指定的条件限制。示例中的条件要求请求的桶名必须是book,对象名必须以user/为前缀,对象的acl必须是公共可读。policy可以限制除了AccessKeyId、Signature、file、policy、token、前缀为"x-ignore-"的字段外的其他所有表单项。下表是conditions中可校验的元素:

表2 policy中可以包含的条件元素

元素名称

元素类型

描述

支持的匹配方式

x-obs-acl

String

请求中的ACL。

精确匹配:Exact Matches

前缀匹配:starts-with

content-length-range

int

设置上传对象的最大和最小长度。例如:

1
["content-length-range", 1048576, 10485760]

范围匹配:Specifying Ranges

Cache-Control, Content-Type, Content-Disposition, Content-Encoding, Expires

String

REST请求特定头域。

精确匹配:Exact Matches

前缀匹配:starts-with

key

String

上传对象的名字。

精确匹配:Exact Matches

前缀匹配:starts-with

bucket

String

请求桶名。

精确匹配:Exact Matches

success_action_redirect

String

上传对象成功后重定向的URL地址。具体描述请参见5.4.2-POST上传

精确匹配:Exact Matches

前缀匹配:starts-with

success_action_status

String

如果未指定success_action_redirect,则成功上传时返回给客户端的状态码。具体描述请参见5.4.2-POST上传

精确匹配:Exact Matches

x-obs-meta-*

String

用户自定义元数据。

元素中的关键字不允许含有非ASCII码或不可识别字符,如果一定要使用非ASCII码或不可识别字符,需要客户端自行做编解码处理,可以采用URL编码或者Base64编码,服务端不会做解码处理。

精确匹配:Exact Matches

前缀匹配:starts-with

x-obs-*

String

其他以x-obs-为前缀的头域。

精确匹配:Exact Matches

前缀匹配:starts-with

x-obs-security-token

String

请求消息头中字段名。

临时AK/SK和securitytoken鉴权必加字段名。如何获取临时AK/SK和securitytoken请参考通过token获取临时访问密钥和securitytoken

精确匹配:Exact Matches

Policy条件匹配的方式如下:

表3 policy条件匹配方式

匹配方式

描述

精确匹配(Exact Matches)

默认是完全匹配,POST表单中该项的值必须和policy的conditions中设置的值完全一样。例如:上传对象的同时设置对象ACL为public-read,表单中x-obs-acl元素的值为public-read,policy中的conditions可以设置为

1
{"x-obs-acl": "public-read" }

或者另一种等效的格式:

1
[ "eq", "$x-obs-acl", "public-read"]

前缀匹配(Starts With)

如果使用该条件,则post表单中对应元素的值必须是固定字符串开始。例如:上传对象名以user/为前缀,表单中key元素的值可以是user/test1、user/test2,policy的conditions中该条件如下:

1
["starts-with", "$key", "user/"]

任意匹配(Matching Any Content)

post表单中对应元素的值可以是任意值。例如:请求成功后重定向的地址可以是任意地址,表单中success_action_redirect元素的值可以是任意值,policy的conditions中该条件如下:

1
["starts-with", "$success_action_redirect", ""]

范围匹配(Specifying Ranges)

post表单中file元素文件的内容长度可以是一个指定的范围,只用于限制对象大小。例如上传对象大小为1-10MB,表单中file元素的内容长度可以是1048576-10485760,policy的conditions中该条件如下,注意值没有双引号:

1
["content-length-range", 1048576, 10485760]

policy使用json格式,conditions可以支持 { } 和 [ ] 两种方式,{ }中包含表单元素的key和value两项,以冒号分隔;[ ]中包含条件类型、key、value三项,以逗号分隔,元素key之前使用$字符表示变量。

Policy中必须转义的字符如下:

表4 policy中必须转义的字符

转义后的字符

真实字符

\\

反斜杠(\)

\$

美元符号($)

\b

退格

\f

换页

\n

换行

\r

回车

\t

水平制表

\v

垂直制表

\uxxxx

所有Unicode字符

步骤二:计算签名

OBS有2种计算表单中携带签名的方式:

表5 计算表单签名的2种方式

计算方式

描述

更多参考

SDK

OBS所有语言的SDK都已实现表单上传签名,无需手动进行签名计算。推荐您直接使用SDK进行接口调用,更方便快捷。

SDK签名实现

手动编码计算签名

按照签名算法手动编码计算签名。

签名算法

SDK签名实现

表6 OBS SDK Header携带签名的实现

SDK

签名实现源文件

Java

AbstractClient.java

Python

client.py

Go

temporary_other.go

C

-

Node.Js

utils.js

Browser.Js

utils.js

PHP

SendRequestTrait.php

.NET

-

签名算法

表单中携带签名的计算公式如下:

Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, StringToSign ) )
StringToSign = Base64( UTF-8-Encoding-Of( policy ) )

签名的计算过程如下:

  1. 构造请求字符串StringToSign,即对policy先进行UTF8编码,然后再进行Base64编码。
  2. 使用SK第一步结果进行HMAC-SHA1签名计算。
  3. 对第二步的结果进行Base64编码,得到签名。
图1 计算表单签名

签名代码示例

以下是计算表单中携带签名的示例代码:

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
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();
    }

    // 构造StringToSign
    private String stringToSign(String[] tmpConditions, String expiration) {
        Listconditions = 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中签名的计算方法

 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
# 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是时间戳形式,例如: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)