使用签名网址。

本页面简要介绍签名网址以及将其用于 Cloud CDN 的说明。拥有签名网址的任何人都会获得限时的资源访问权限,无论该用户是否拥有 Google 账号都是如此。

签名网址提供可用于发出请求的有限权限和时间。签名网址的查询字符串中包含身份验证信息,这样,用户不需要提供凭据也可对资源执行特定操作。 生成签名网址时,您需要指定一个用户或服务账号,且该用户或服务账号具有的权限必须足以发出与该网址相关的请求。

生成签名网址后,拥有该网址的任何人都可以使用该网址,在指定的时间段内执行指定的操作(例如读取对象)。

签名网址还支持可选的 URLPrefix 参数,允许您提供访问具有相同前缀的多个网址的权限。

如果要限制对特定网址前缀的访问权限,请考虑使用签名 Cookie。

准备工作

在使用签名网址之前,请执行以下操作:

  • 确保已启用 Cloud CDN;如需了解相关说明,请参阅使用 Cloud CDN。您可以先在后端配置签名网址,然后再启用 Cloud CDN;在启用 Cloud CDN 之前,相关配置不会生效。

  • 如有必要,请更新到 Google Cloud CLI 的最新版本:

    gcloud components update
    

如需查看概览,请参阅签名网址和签名 Cookie。

配置签名请求密钥

为签名网址或签名 Cookie 创建密钥需要执行几个步骤,详情请参阅下面几部分内容。

安全注意事项

在下列情况下,Cloud CDN 不会验证请求:

  • 请求未签名。
  • 请求的后端服务或后端存储桶未启用 Cloud CDN。

系统必须始终先在源站验证签名的请求,然后再传送响应。这是因为源站可用于传送签名的和未签名的混合内容,而且客户端可能会直接访问源站。

  • Cloud CDN 不会屏蔽没有 Signature 查询参数或 Cloud-CDN-Cookie HTTP Cookie 的请求。它会拒绝请求参数无效(或格式不正确)的请求。
  • 当您的应用检测到无效签名时,请确保其使用 HTTP 403 (Unauthorized) 响应代码来作出响应。HTTP 403 响应代码不可缓存。
  • 签名请求和未签名请求的响应会单独缓存,因此对有效签名请求的成功响应绝不会用于传送未签名的请求。
  • 如果您的应用向无效请求发送了可缓存的响应代码,则未来的有效请求可能会被错误地拒绝。

对于 Cloud Storage 后端,请务必移除公共访问权限,以便 Cloud Storage 可以拒绝缺少有效签名的请求。

下表总结了这种行为。

请求具有签名 缓存命中 行为
否 否 转发到后端源站。
否 是 从缓存传送。
是 否 验证签名。如果有效,则转发到后端源站。
是 是 验证签名。如果有效,则从缓存传送。

创建签名请求密钥

如需启用对 Cloud CDN 签名网址和签名 Cookie 的支持,您可以对启用了 Cloud CDN 的后端服务和/或后端存储分区创建一个或多个密钥。

您可以根据自己的安全需求,为每个后端服务或后端存储桶创建和删除密钥。每个后端最多可以同时配置三个密钥。我们建议您定期轮换密钥,即删除最旧的密钥,然后添加新密钥,并在为网址或 Cookie 签名时使用新密钥。

您可以在多个后端服务和后端存储分区中使用相同的密钥名称,因为各组密钥是彼此独立的。密钥名称最长为 63 个字符。要命名密钥,请使用字符 A-Z、a-z、0-9、_(下划线)、-(连字符)。

创建密钥后,务必妥善保管好密钥,因为只要拥有其中一个密钥,任何人都可以创建 Cloud CDN 接受的签名网址或签名 Cookie,直到该密钥从 Cloud CDN 中删除为止。密钥存储在生成签名网址或签名 Cookie 的计算机上。Cloud CDN 也会存储这些密钥以验证请求签名。

为了确保密钥的机密性,请勿在任何 API 请求的响应中包含密钥值。如果您丢失了某个密钥,则必须创建一个新密钥。

如需创建签名请求密钥,请按照以下步骤操作。

控制台

  1. 在 Google Cloud 控制台中,前往 Cloud CDN 页面。

    转到 Cloud CDN

  2. 点击您要为其添加密钥的来源的名称。
  3. 在源站详情页面上,点击修改按钮。
  4. 在 Origin basics(来源基本信息)部分中,点击下一步以打开主机和路径规则部分。
  5. 在主机和路径规则部分中,点击下一步以打开缓存性能部分。
  6. 在受限内容部分中,选择使用签名网址和签名 Cookie 限制访问权限。
  7. 点击添加签名密钥。

    1. 为新签名密钥指定一个唯一的名称。
    2. 在密钥创建方法部分中,选择自动生成。或者,点击让我输入,然后指定签名键值。

      对于前一个选项,请将自动生成的签名密钥值复制到一个私有文件中,您可以使用该值来创建签名网址。

    3. 点击完成。

    4. 在缓存条目存在时间上限部分,输入一个值,然后选择一个时间单位。

  8. 点击完成。

gcloud

gcloud 命令行工具会从您指定的本地文件中读取密钥。密钥文件必须按照如下方式创建:生成强随机 128 位代码,使用 base64 对该代码进行编码,然后将字符 + 替换为 -、将字符 / 替换为 _。如需了解详情,请参阅 RFC 4648。必须确保密钥具有强随机性。在类似 UNIX 的系统上,您可以使用以下命令生成强随机密钥并将密钥存储在密钥文件中:

head -c 16 /dev/urandom | base64 | tr +/ -_ > KEY_FILE_NAME

如需将密钥添加到后端服务,请执行以下操作:

gcloud compute backend-services \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

如需将密钥添加到后端存储分区,请执行以下操作:

gcloud compute backend-buckets \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

配置 Cloud Storage 权限

如果您使用 Google Cloud Storage,并且限制了可以读取对象的用户,则必须向 Cloud CDN 授予读取对象的权限,方法是将 Cloud CDN 服务账号添加到 Cloud Storage ACL 中。

您无需创建服务账号。当您首次将密钥添加到项目中的后端存储分区时,系统将自动创建服务账号。

在运行以下命令之前,请向项目中的后端存储分区添加至少一个密钥。否则,该命令会失败并显示错误,这是因为您必须先为项目添加一个或多个密钥,系统才会创建 Cloud CDN 缓存填充服务账号。

gcloud storage buckets add-iam-policy-binding gs://BUCKET \
  --member=serviceAccount:service-PROJECT_NUMBER@cloud-cdn-fill. \
  --role=roles/storage.objectViewer

将 PROJECT_NUMBER 替换为您的项目编号,将 BUCKET 替换为您的存储桶。

Cloud CDN 服务账号 service-PROJECT_NUMBER@cloud-cdn-fill. 不会出现在您项目的服务账号列表中。这是因为 Cloud CDN 服务账号归 Cloud CDN 所有,不属于您的项目。

如需详细了解项目编号,请参阅 Google Cloud 控制台帮助文档中的查找项目 ID 和项目编号。

自定义最长缓存时间

无论后端的 Cache-Control 标头如何,Cloud CDN 都会缓存针对签名请求的响应。响应无需重新验证的最长缓存时间由 signed-url-cache-max-age 标志设置,默认设为 1 小时,您可以按照此处所示的方法进行修改。

要设置后端服务或后端存储分区的最长缓存时间,请运行以下命令之一:

gcloud compute backend-services update BACKEND_NAME \
  --signed-url-cache-max-age MAX_AGE
gcloud compute backend-buckets update BACKEND_NAME \
  --signed-url-cache-max-age MAX_AGE

列出签名请求密钥名称

要列出后端服务或后端存储分区上的密钥,请运行以下命令之一:

gcloud compute backend-services describe BACKEND_NAME
gcloud compute backend-buckets describe BACKEND_NAME

删除签名请求密钥

如果采用特定密钥签名的网址不再受到支持,请运行以下某个命令以从后端服务或后端存储分区中删除该密钥:

gcloud compute backend-services \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME
gcloud compute backend-buckets \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME

为网址签名

最后一步是对网址进行签名并分发网址。您可以使用 gcloud compute sign-url 命令或使用自己编写的代码对网址进行签名。如果您需要大量签名网址,则自定义代码的性能更佳。

创建签名网址

按照以下说明使用 gcloud compute sign-url 命令创建签名网址。此步骤假定您已创建密钥。

控制台

您无法使用 Google Cloud 控制台创建签名网址,您可以使用 Google Cloud CLI,也可以使用以下示例编写自定义代码。

gcloud

Google Cloud CLI 包含用于对网址进行签名的命令。该命令可实现编写自己的代码部分所述的算法。

gcloud compute sign-url \
  "URL" \
  --key-name KEY_NAME \
  --key-file KEY_FILE_NAME \
  --expires-in TIME_UNTIL_EXPIRATION \
  [--validate]

该命令会读取 KEY_FILE_NAME 中采用 base64url 编码的密钥值并对其进行解码,然后输出一个签名网址;您可以将该签名网址用于针对指定网址发出的 GET 或 HEAD 请求。

例如:

gcloud compute sign-url \
  "https://example.com/media/video.mp4" \
  --key-name my-test-key \
  --expires-in 30m \
  --key-file sign-url-key-file

URL 必须是包含路径某个组成部分的有效网址。例如,http://example.com 无效,不过 https://example.com/ 和 https://example.com/whatever 均为有效网址。

如果指定了可选的 --validate 标志,则此命令会发送包含生成的网址的 HEAD 请求,并输出 HTTP 响应代码。如果签名网址是正确的,则响应代码与您的后端发送的结果代码相同。如果响应代码不相同,请重新检查 KEY_NAME 以及指定的文件的内容,并确保 TIME_UNTIL_EXPIRATION 的值至少有几秒钟。

如果未指定 --validate 标志,则系统不会验证以下内容:

  • 输入
  • 生成的网址
  • 生成的签名网址

以程序化方式创建签名网址

以下代码示例演示了如何以编程方式创建签名网址。

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

// SignURL creates a signed URL for an endpoint on Cloud CDN.
//
// - url must start with "https://" and should not have the "Expires", "KeyName", or "Signature"
// query parameters.
// - key should be in raw form (not base64url-encoded) which is 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signURL(url, keyName string, key []byte, expiration time.Time) string {
	sep := "?"
	if strings.Contains(url, "?") {
		sep = "&"
	}
	url += sep
	url += fmt.Sprintf("Expires=%d", expiration.Unix())
	url += fmt.Sprintf("&KeyName=%s", keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(url))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))
	url += fmt.Sprintf("&Signature=%s", sig)
	return url
}

Ruby

def signed_url url:, key_name:, key:, expiration:
  # url        = "URL of the endpoint served by Cloud CDN"
  # key_name   = "Name of the signing key added to the Google Cloud Storage bucket or service"
  # key        = "Signing key as urlsafe base64 encoded string"
  # expiration = Ruby Time object with expiration time

  require "base64"
  require "openssl"
  require "time"

  # Decode the URL safe base64 encode key
  decoded_key = Base64.urlsafe_decode64 key

  # Get UTC time in seconds
  expiration_utc = expiration.utc.to_i

  # Determine which separator makes sense given a URL
  separator = "?"
  separator = "&" if url.include? "?"

  # Concatenate url with expected query parameters Expires and KeyName
  url = "#{url}#{separator}Expires=#{expiration_utc}&KeyName=#{key_name}"

  # Sign the url using the key and url safe base64 encode the signature
  signature         = OpenSSL::HMAC.digest "SHA1", decoded_key, url
  encoded_signature = Base64.urlsafe_encode64 signature

  # Concatenate the URL and encoded signature
  signed_url = "#{url}&Signature=#{encoded_signature}"
end

.NET

        /// <summary>
        /// Creates signed URL for Google Cloud SDN
        /// More details about order of operations is here: 
        /// <see cref="https://cloud.google.com/cdn/docs/using-signed-urls#programmatically_creating_signed_urls"/>
        /// </summary>
        /// <param name="url">The Url to sign. This URL can't include Expires and KeyName query parameters in it</param>
        /// <param name="keyName">The name of the key used to sign the URL</param>
        /// <param name="encodedKey">The key used to sign the Url</param>
        /// <param name="expirationTime">Expiration time of the signature</param>
        /// <returns>Signed Url that is valid until {expirationTime}</returns>
        public static string CreateSignedUrl(string url, string keyName, string encodedKey, DateTime expirationTime)
        {
            var builder = new UriBuilder(url);

            long unixTimestampExpiration = ToUnixTime(expirationTime);

            char queryParam = string.IsNullOrEmpty(builder.Query) ? '?' : '&';
            builder.Query += $"{queryParam}Expires={unixTimestampExpiration}&KeyName={keyName}".ToString();

            // Key is passed as base64url encoded
            byte[] decodedKey = Base64UrlDecode(encodedKey);

            // Computes HMAC SHA-1 hash of the URL using the key
            byte[] hash = ComputeHash(decodedKey, builder.Uri.AbsoluteUri);
            string encodedHash = Base64UrlEncode(hash);

            builder.Query += $"&Signature={encodedHash}";
            return builder.Uri.AbsoluteUri;
        }

        private static long ToUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date - epoch).TotalSeconds);
        }

        private static byte[] Base64UrlDecode(string arg)
        {
            string s = arg;
            s = s.Replace('-', '+'); // 62nd char of encoding
            s = s.Replace('_', '/'); // 63rd char of encoding

            return Convert.FromBase64String(s); // Standard base64 decoder
        }

        private static string Base64UrlEncode(byte[] inputBytes)
        {
            var output = Convert.ToBase64String(inputBytes);

            output = output.Replace('+', '-')      // 62nd char of encoding
                           .Replace('/', '_');     // 63rd char of encoding

            return output;
        }

        private static byte[] ComputeHash(byte[] secretKey, string signatureString)
        {
            var enc = Encoding.ASCII;
            using (HMACSHA1 hmac = new HMACSHA1(secretKey))
            {
                hmac.Initialize();

                byte[] buffer = enc.GetBytes(signatureString);

                return hmac.ComputeHash(buffer);
            }
        }

Java

/** Samples to create a signed URL for a Cloud CDN endpoint */
public class SignedUrls {

  /**
   * Creates a signed URL for a Cloud CDN endpoint with the given key
   * URL must start with http:// or https://, and must contain a forward
   * slash (/) after the hostname.
   *
   * @param url the Cloud CDN endpoint to sign
   * @param key url signing key uploaded to the backend service/bucket, as a 16-byte array
   * @param keyName the name of the signing key added to the back end bucket or service
   * @param expirationTime the date that the signed URL expires
   * @return a properly formatted signed URL
   * @throws InvalidKeyException when there is an error generating the signature for the input key
   * @throws NoSuchAlgorithmException when HmacSHA1 algorithm is not available in the environment
   */
  public static String signUrl(String url,
                               byte[] key,
                               String keyName,
                               Date expirationTime)
          throws InvalidKeyException, NoSuchAlgorithmException {

    final long unixTime = expirationTime.getTime() / 1000;

    String urlToSign = url
                        + (url.contains("?") ? "&" : "?")
                        + "Expires=" + unixTime
                        + "&KeyName=" + keyName;

    String encoded = SignedUrls.getSignature(key, urlToSign);
    return urlToSign + "&Signature=" + encoded;
  }

  public static String getSignature(byte[] privateKey, String input)
      throws InvalidKeyException, NoSuchAlgorithmException {

    final String algorithm = "HmacSHA1";
    final int offset = 0;
    Key key = new SecretKeySpec(privateKey, offset, privateKey.length, algorithm);
    Mac mac = Mac.getInstance(algorithm);
    mac.init(key);
    return  Base64.getUrlEncoder().encodeToString(mac.doFinal(input.getBytes()));
  }

Python

import argparse
import base64
from datetime import datetime, timezone
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


def sign_url(
    url: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed URL string for the specified URL and configuration.

    Args:
        url: URL to sign.
        key_name: name of the signing key.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as time-zone aware datetime.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified configuration.
    """
    stripped_url = url.strip()
    parsed_url = urlsplit(stripped_url)
    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
    epoch = datetime.fromtimestamp(0, timezone.utc)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    url_to_sign = f"{stripped_url}{'&' if query_params else '?'}Expires={expiration_timestamp}&KeyName={key_name}"

    digest = hmac.new(decoded_key, url_to_sign.encode("utf-8"), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")

    return f"{url_to_sign}&Signature={signature}"

PHP

/**
 * Decodes base64url (RFC4648 Section 5) string
 *
 * @param string $input base64url encoded string
 *
 * @return string
 */
function base64url_decode($input)
{
    $input .= str_repeat('=', (4 - strlen($input) % 4) % 4);
    return base64_decode(strtr($input, '-_', '+/'), true);
}

/**
* Encodes a string with base64url (RFC4648 Section 5)
* Keeps the '=' padding by default.
*
* @param string $input   String to be encoded
* @param bool   $padding Keep the '=' padding
*
* @return string
*/
function base64url_encode($input, $padding = true)
{
    $output = strtr(base64_encode($input), '+/', '-_');
    return ($padding) ? $output : str_replace('=', '',  $output);
}

/**
 * Creates signed URL for Google Cloud CDN
 * Details about order of operations: https://cloud.google.com/cdn/docs/using-signed-urls#creating_signed_urls
 *
 * Example function invocation (In production store the key safely with other secrets):
 *
 *     <?php
 *     $base64UrlKey = 'wpLL7f4VB9RNe_WI0BBGmA=='; // head -c 16 /dev/urandom | base64 | tr +/ -_
 *     $signedUrl = sign_url('https://example.com/foo', 'my-key', $base64UrlKey, time() + 1800);
 *     echo $signedUrl;
 *     ?>
 *
 * @param string $url             URL of the endpoint served by Cloud CDN
 * @param string $keyName         Name of the signing key added to the Google Cloud Storage bucket or service
 * @param string $base64UrlKey    Signing key as base64url (RFC4648 Section 5) encoded string
 * @param int    $expirationTime  Expiration time as a UNIX timestamp (GMT, e.g. time())
 *
 * @return string
 */
function sign_url($url, $keyName, $base64UrlKey, $expirationTime)
{
    // Decode the key
    $decodedKey = base64url_decode($base64UrlKey);

    // Determine which separator makes sense given a URL
    $separator = (strpos($url, '?') === false) ? '?' : '&';

    // Concatenate url with expected query parameters Expires and KeyName
    $url = "{$url}{$separator}Expires={$expirationTime}&KeyName={$keyName}";

    // Sign the url using the key and encode the signature using base64url
    $signature = hash_hmac('sha1', $url, $decodedKey, true);
    $encodedSignature = base64url_encode($signature);

    // Concatenate the URL and encoded signature
    return "{$url}&Signature={$encodedSignature}";
}

以编程方式创建具有网址前缀的签名网址

以下代码示例演示了如何以编程方式创建具有网址前缀的签名网址。

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

// SignURLWithPrefix creates a signed URL prefix for an endpoint on Cloud CDN.
// Prefixes allow access to any URL with the same prefix, and can be useful for
// granting access broader content without signing multiple URLs.
//
// - urlPrefix must start with "https://" and should not include query parameters.
// - key should be in raw form (not base64url-encoded) which is 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signURLWithPrefix(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	if strings.Contains(urlPrefix, "?") {
		return "", fmt.Errorf("urlPrefix must not include query params: %s", urlPrefix)
	}

	encodedURLPrefix := base64.URLEncoding.EncodeToString([]byte(urlPrefix))
	input := fmt.Sprintf("URLPrefix=%s&Expires=%d&KeyName=%s",
		encodedURLPrefix, expiration.Unix(), keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(input))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

	signedValue := fmt.Sprintf("%s&Signature=%s", input, sig)

	return signedValue, nil
}

Java

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SignedUrlWithPrefix {

  public static void main(String[] args) throws Exception {
    // TODO(developer): Replace these variables before running the sample.

    // The name of the signing key must match a key added to the back end bucket or service.
    String keyName = "YOUR-KEY-NAME";
    // Path to the URL signing key uploaded to the backend service/bucket.
    String keyPath = "/path/to/key";
    // The date that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // URL of request
    String requestUrl = "https://media.example.com/videos/id/main.m3u8?userID=abc123&starting_profile=1";
    // URL prefix to sign as a string. URL prefix must start with either "http://" or "https://"
    // and must not include query parameters.
    String urlPrefix = "https://media.example.com/videos/";

    // Read the key as a base64 url-safe encoded string, then convert to byte array.
    // Key used in signing must be in raw form (not base64url-encoded).
    String base64String = new String(Files.readAllBytes(Paths.get(keyPath)),
        StandardCharsets.UTF_8);
    byte[] keyBytes = Base64.getUrlDecoder().decode(base64String);

    // Sign the url with prefix
    String signUrlWithPrefixResult = signUrlWithPrefix(requestUrl,
        urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signUrlWithPrefixResult);
  }

  // Creates a signed URL with a URL prefix for a Cloud CDN endpoint with the given key. Prefixes
  // allow access to any URL with the same prefix, and can be useful for granting access broader
  // content without signing multiple URLs.
  static String signUrlWithPrefix(String requestUrl, String urlPrefix, byte[] key, String keyName,
      long expirationTime)
      throws InvalidKeyException, NoSuchAlgorithmException {

    // Validate input URL prefix.
    try {
      URL validatedUrlPrefix = new URL(urlPrefix);
      if (!validatedUrlPrefix.getProtocol().startsWith("http")) {
        throw new IllegalArgumentException(
            "urlPrefix must start with either http:// or https://: " + urlPrefix);
      }
      if (validatedUrlPrefix.getQuery() != null) {
        throw new IllegalArgumentException("urlPrefix must not include query params: " + urlPrefix);
      }
    } catch (MalformedURLException e) {
      throw new IllegalArgumentException("urlPrefix malformed: " + urlPrefix);
    }

    String encodedUrlPrefix = Base64.getUrlEncoder().encodeToString(urlPrefix.getBytes(
        StandardCharsets.UTF_8));
    String urlToSign = "URLPrefix=" + encodedUrlPrefix
        + "&Expires=" + expirationTime
        + "&KeyName=" + keyName;

    String encoded = getSignatureForUrl(key, urlToSign);
    return requestUrl + "&" + urlToSign + "&Signature=" + encoded;
  }

  // Creates signature for input url with private key.
  private static String getSignatureForUrl(byte[] privateKey, String input)
      throws InvalidKeyException, NoSuchAlgorithmException {

    final String algorithm = "HmacSHA1";
    final int offset = 0;
    Key key = new SecretKeySpec(privateKey, offset, privateKey.length, algorithm);
    Mac mac = Mac.getInstance(algorithm);
    mac.init(key);
    return Base64.getUrlEncoder()
        .encodeToString(mac.doFinal(input.getBytes(StandardCharsets.UTF_8)));
  }
}

Python

import argparse
import base64
from datetime import datetime, timezone
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


def sign_url_prefix(
    url: str,
    url_prefix: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed URL string for the specified URL prefix and configuration.

    Args:
        url: URL of request.
        url_prefix: URL prefix to sign.
        key_name: name of the signing key.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as time-zone aware datetime.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified URL prefix and configuration.
    """
    stripped_url = url.strip()
    parsed_url = urlsplit(stripped_url)
    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
    encoded_url_prefix = base64.urlsafe_b64encode(
        url_prefix.strip().encode("utf-8")
    ).decode("utf-8")
    epoch = datetime.fromtimestamp(0, timezone.utc)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy = f"URLPrefix={encoded_url_prefix}&Expires={expiration_timestamp}&KeyName={key_name}"

    digest = hmac.new(decoded_key, policy.encode("utf-8"), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")

    return f"{stripped_url}{'&' if query_params else '?'}{policy}&Signature={signature}"

生成自定义签名网址

当您编写自己的代码来生成签名网址时,您的目标是使用以下格式或算法来创建网址;所有网址参数均区分大小写,并且必须符合所示的顺序:

https://example.com/foo?Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

要生成签名网址,请按以下步骤操作:

  1. 确保用于签名的网址不带 Signature 查询参数。

  2. 确定网址的到期时间,并附加包含所需到期时间的 Expires 查询参数。到期时间以世界协调时间 (UTC) 为准,表示从世界协调时间 (UTC) 1970-01-01 00:00:00 开始计算的秒数。为了最大限度地提高安全性,请将此值设置为适用于您的用例的最短时间段。签名网址的有效期越长,从您这里获得该网址的用户将网址分享给其他用户的风险就越大(无论有意还是无意分享)。

  3. 设置密钥名称。您必须使用传送网址的后端服务或后端存储桶的密钥对该网址进行签名。最好使用最近添加的签名网址密钥来进行密钥轮替。通过附加 &KeyName=KEY_NAME 将密钥添加到网址中。将 KEY_NAME 替换为在创建签名请求密钥中创建的所选密钥的名称。

  4. 为网址签名。按照如下步骤创建签名网址。确保查询参数遵循第 1 步之前显示的顺序,并确保签名网址中的任何内容都没有改变大小写。

    a. 使用 HMAC-SHA1 对整个网址(包括开头的 http:// 或 https:// 以及末尾的 &KeyName...)进行哈希处理,并使用与上述选定密钥名称所对应的密钥。使用原始的 16 字节密钥,而非 base64url 编码密钥。如有必要,请进行解码。

    b. 使用 base64url 编码对结果进行编码。

    c. 在网址中附加 &Signature=,后跟经过编码的签名。 请勿将签名末尾的 = 字符转换为百分比编码形式 %3D。

为签名网址使用网址前缀

您不必为带有 Expires 和 KeyName 查询参数的完整请求网址签名,而是可以仅对 URLPrefix、Expires 和 KeyName 查询参数进行签名。这样一来,您就可以在多个与 URLPrefix 相匹配的网址中重复使用 URLPrefix、Expires、KeyName 和 Signature 查询参数的给定组合,而不需要为每个不同网址创建一个新签名。

在以下示例中,突出显示的文本是您签名的参数。Signature 照常作为最后一个参数附加。

https://media.example.com/videos/id/master.m3u8?userID=abc123&starting_profile=1&URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv&Expires=1566268009&KeyName=mySigningKey&Signature=8NBSdQGzvDftrOIa3WHpp646Iis=

与对完整请求网址进行签名不同,使用 URLPrefix 签名时,您并非对任何查询参数进行签名,因此查询参数可以自由添加到网址中。与完整的请求网址签名不同,这些额外的查询参数可以同时出现在构成签名的查询参数之前和之后。因此,以下内容也是包含签名网址前缀的有效网址:

https://media.example.com/videos/id/master.m3u8?userID=abc123&URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv&Expires=1566268009&KeyName=mySigningKey&Signature=8NBSdQGzvDftrOIa3WHpp646Iis=&starting_profile=1

URLPrefix 表示可在网址中安全使用的 Base64 编码网址前缀,其中包含该签名应对其有效的所有路径。

URLPrefix 会对架构(http:// 或 https://)、FQDN 和可选路径进行编码。您可以选择是否使用以 / 结尾的路径,但我们建议您使用。前缀不应包含查询参数或者 ? 或 # 这样的片段。

例如,https://media.example.com/videos 会将请求匹配到以下两项:

  • https://media.example.com/videos?video_id=138183&user_id=138138
  • https://media.example.com/videos/137138595?quality=low

前缀的路径作为文本子字符串使用,而不是严格意义上的目录路径。 例如,前缀 https://example.com/data 会授予对以下两个路径的访问权限:

  • /data/file1
  • /database

要避免这种错误,我们建议您在所有前缀的结尾处使用 /,除非您有意选择使用部分文件名(例如 https://media.example.com/videos/123)作为前缀的结尾,以授予对以下路径的访问权限:

  • /videos/123_chunk1
  • /videos/123_chunk2
  • /videos/123_chunkN

如果请求的网址与 URLPrefix 不匹配,Cloud CDN 会拒绝请求并向客户端返回一条 HTTP 403 错误。

验证签名网址

验证签名网址的过程与生成签名网址的过程基本相同。例如,假设您要验证以下签名网址:

https://example.com/PATH?Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

您可以使用名为 KEY_NAME 的 Secret 密钥来为以下网址单独生成签名:

https://example.com/PATH?Expires=EXPIRATION&KeyName=KEY_NAME

然后,您便可以验证其与 SIGNATURE 是否匹配。

假设您要验证具有 URLPrefix 的签名网址,如下所示:

https://example.com/PATH?URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

首先,验证 URL_PREFIX 的 base64-decoded 值是否为 https://example.com/PATH 的前缀。在这种情况下,您可以计算以下内容的签名:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

然后,您可以验证其是否匹配 SIGNATURE。

对于基于网址的签名方法,如果签名是查询参数的一部分或嵌入为网址路径组件,则在将请求发送到源站之前,系统会从网址中移除签名和相关参数。这样可以防止签名在源站处理请求时导致路由问题。如需验证这些请求,您可以检查 x-client-request-url 请求标头,其中包含移除签名组件之前的原始(已签名)客户端请求网址。

移除对 Cloud Storage 存储桶的公开访问权限

为确保签名网址正确地保护内容,请务必不要让源服务器授予对内容的公共访问权限。使用 Cloud Storage 存储分区时,出于测试目的临时性地将一些对象公开是一种常见做法。启用签名网址后,请务必移除对该存储桶的 allUsers 和 allAuthenticatedUsers(如适用)READ 权限(也就是 Storage Object Viewer Identity and Access Management 角色)。

停用对存储分区的公开访问权限后,个别用户仍可以在无签名网址的情况下访问 Cloud Storage,前提是他们拥有访问权限,例如 OWNER 权限。

如需移除对 Cloud Storage 存储分区的公共 allUsers READ 权限,请逆向执行将存储分区中的所有对象设为可公开读取中所述的操作。

分发和使用签名网址

从 Google Cloud CLI 返回的网址或由您的自定义代码生成的网址可根据您的需求进行分发。我们建议仅对 HTTPS 网址进行签名,因为 HTTPS 提供的安全传输可防止签名网址中的 Signature 组成部分遭到拦截。同样,您应该通过安全传输协议(例如 TLS/HTTPS)分发签名网址。