使用自己的程序执行 V4 签名流程

本页面介绍了实现 V4 签名过程的算法,以便您可以使用所选编程语言在自己的工作流程中创建 Cloud Storage RSA 密钥签名网址

须知事项

在创建实现 V4 签名流程的程序之前,您必须完成以下步骤:

  1. 创建服务账号。如果您已有服务账号,则可以跳过此步骤。

  2. 向服务账户授予足够的权限,使其能够执行签名网址将发出的请求。

    例如,如果签名网址允许用户读取对象数据,则服务账号本身必须具有读取对象数据的权限。

  3. 如果您要使用的签名算法未内置到 Google Cloud 中,请生成新的私钥,或已有服务账号的私钥。密钥可以是 JSON 或 PKCS12 格式。

网址签名算法

您的程序应包含以下步骤:

  1. 规范化请求构造为字符串。规范化请求定义用户在使用您的签名网址时,其请求中必须包含的元素。

    如需详细了解所需的请求内容和格式,请参阅规范化请求

  2. 构造待签名字符串。 待签名字符串是创建签名的基础字符串,其中包含规范化请求的十六进制编码哈希值。

    如需详细了解待签名字符串的格式,请参阅签名

  3. 将 RSA 签名与 SHA-256 搭配使用,对待签名字符串进行签名。签名的结果是您的请求签名。 您可以通过多种方式进行签名:

  4. 使用以下连接构造签名网址:

    HOSTNAME + PATH_TO_RESOURCE + "?" + CANONICAL_QUERY_STRING + "&X-Goog-Signature=" + REQUEST_SIGNATURE

    签名网址包含以下组成部分:

    • HOSTNAME:这应该为 https://storage.googleapis.com

    • PATH_TO_RESOURCE:这应该与您在构造规范化请求时使用的值相匹配。

    • CANONICAL_QUERY_STRING:这应该与您在构造规范化请求时使用的值相匹配。

    • REQUEST_SIGNATURE:这是上一步中使用 RSA 签名的输出。

    完成后的网址示例如下所示:

    https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=GOOG4-
    RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount.com
    %2F20181026%2Fus%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T211942Z&X-Goog
    -expires=3600&X-Goog-Signedheaders=host&X-Goog-Signature=2d2a6f5055eb004b8690b
    9479883292ae7450cdc15f17d7f99bc49b916f9e7429106ed7e5858ae6b4ab0bbbdb1a8ccc364d
    ad3a0da2caebd30887a70c5b2569d089ceb8afbde3eed4dff5086f0db5483998c175980991fe89
    9fbd2cd8cb813b00165e8d56e0a8aa7b3d7a12ee1baa8400611040f05b50a1a8eab5ba223fe137
    5747748de950ec7a4dc50f8382a6ffd4994ac42498d7daa703d9a414d4475154d0e7edaa92d4f2
    507d92c1f7e8efa7cab64df68b5df48575b9259d8d0bdb5dc752bdf07bd162d98ff2924f2e4a26
    fa6b3cede73ad5333c47d146a21c2ab2d97115986a12c68ff37346d6c2ca83e56b8ec8ad956327
    10b489b75c35697d781c38e

Python 示例程序

您可以使用 Cloud Storage 客户端库为许多常用编程语言创建签名网址。如需查看示例,请参阅使用 Cloud Storage 工具执行 V4 签名流程

以下示例展示了未使用 Cloud Storage 客户端库的网址签名算法实现。该示例使用的是 Python 编程语言,但可以根据您选择的语言进行调整。

import binascii
import collections
import datetime
import hashlib
import sys
from urllib.parse import quote

# pip install google-auth
from google.oauth2 import service_account

# pip install six
import six

def generate_signed_url(
    service_account_file,
    bucket_name,
    object_name,
    subresource=None,
    expiration=604800,
    http_method="GET",
    query_parameters=None,
    headers=None,
):
    if expiration > 604800:
        print("Expiration Time can't be longer than 604800 seconds (7 days).")
        sys.exit(1)

    escaped_object_name = quote(six.ensure_binary(object_name), safe=b"/~")
    canonical_uri = f"/{escaped_object_name}"

    datetime_now = datetime.datetime.now(tz=datetime.timezone.utc)
    request_timestamp = datetime_now.strftime("%Y%m%dT%H%M%SZ")
    datestamp = datetime_now.strftime("%Y%m%d")

    google_credentials = service_account.Credentials.from_service_account_file(
        service_account_file
    )
    client_email = google_credentials.service_account_email
    credential_scope = f"{datestamp}/auto/storage/goog4_request"
    credential = f"{client_email}/{credential_scope}"

    if headers is None:
        headers = dict()
    host = f"{bucket_name}.storage.googleapis.com"
    headers["host"] = host

    canonical_headers = ""
    ordered_headers = collections.OrderedDict(sorted(headers.items()))
    for k, v in ordered_headers.items():
        lower_k = str(k).lower()
        strip_v = str(v).lower()
        canonical_headers += f"{lower_k}:{strip_v}\n"

    signed_headers = ""
    for k, _ in ordered_headers.items():
        lower_k = str(k).lower()
        signed_headers += f"{lower_k};"
    signed_headers = signed_headers[:-1]  # remove trailing ';'

    if query_parameters is None:
        query_parameters = dict()
    query_parameters["X-Goog-Algorithm"] = "GOOG4-RSA-SHA256"
    query_parameters["X-Goog-Credential"] = credential
    query_parameters["X-Goog-Date"] = request_timestamp
    query_parameters["X-Goog-Expires"] = expiration
    query_parameters["X-Goog-SignedHeaders"] = signed_headers
    if subresource:
        query_parameters[subresource] = ""

    canonical_query_string = ""
    ordered_query_parameters = collections.OrderedDict(sorted(query_parameters.items()))
    for k, v in ordered_query_parameters.items():
        encoded_k = quote(str(k), safe="")
        encoded_v = quote(str(v), safe="")
        canonical_query_string += f"{encoded_k}={encoded_v}&"
    canonical_query_string = canonical_query_string[:-1]  # remove trailing '&'

    canonical_request = "\n".join(
        [
            http_method,
            canonical_uri,
            canonical_query_string,
            canonical_headers,
            signed_headers,
            "UNSIGNED-PAYLOAD",
        ]
    )

    canonical_request_hash = hashlib.sha256(canonical_request.encode()).hexdigest()

    string_to_sign = "\n".join(
        [
            "GOOG4-RSA-SHA256",
            request_timestamp,
            credential_scope,
            canonical_request_hash,
        ]
    )

    # signer.sign() signs using RSA-SHA256 with PKCS1v15 padding
    signature = binascii.hexlify(
        google_credentials.signer.sign(string_to_sign)
    ).decode()

    scheme_and_host = "{}://{}".format("https", host)
    signed_url = "{}{}?{}&x-goog-signature={}".format(
        scheme_and_host, canonical_uri, canonical_query_string, signature
    )

    return signed_url

后续步骤