使用已簽署的網址

本頁面概述已簽署的網址,並提供如何在 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. 在「來源基本資訊」部分,按一下「Next」,開啟「Host and path rules」部分。
  5. 在「主機和路徑規則」部分中,按一下「下一步」,開啟「快取效能」部分。
  6. 在「受限制的內容」部分,選取「透過已簽署的網址和 Cookie 限制存取」
  7. 按一下「新增簽署金鑰」

    1. 為新的簽署金鑰指定專屬名稱。
    2. 在「Key creation method」部分,選取「Automatically generate」。或者,您也可以按一下「讓我輸入」,然後指定簽署金鑰值。

      如果選擇前者,請將自動產生的簽署金鑰值複製到私密檔案,以便建立已簽署的網址

    3. 按一下 [完成]

    4. 在「Cache entry maximum age」(快取項目存在時間長度上限) 部分輸入值,然後選取時間單位。

  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 權限

如果您使用的是 Cloud Storage,並已限制哪些使用者可以讀取物件,則必須將 Cloud CDN 服務帳戶新增至 Cloud Storage ACL,藉此授予 Cloud CDN 讀取物件的權限。

您不需要建立服務帳戶。在您首次將金鑰新增至專案後端值區時,系統就會自動建立服務帳戶。

請先在專案的後端值區中加入至少一個金鑰,再執行下列指令。否則,指令就會失敗並發生錯誤,因為您必須先為專案新增一或多個金鑰,系統才會建立 Cloud CDN 快取填補服務帳戶。

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

PROJECT_NUM 替換為專案編號,並將 BUCKET 替換為儲存值區。

Cloud CDN 服務帳戶 service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com 不會顯示在專案的服務帳戶清單中。這是因為 Cloud CDN 服務帳戶為 Cloud CDN 所擁有,而非為您的專案所擁有。

如要進一步瞭解專案編號,請參閱 Google Cloud 主控台說明文件中的「尋找專案 ID 與專案編號」一節。

自訂快取時間上限

無論後端的 Cache-Control 標頭為何,Cloud CDN 都會快取已簽署要求的回應。系統會使用 signed-url-cache-max-age 標記設定回應可快取的時間上限,預設為一小時,但您可以按照下列步驟修改這項設定。

如要設定後端服務或後端值區的快取時間長度上限,請執行下列任一指令:

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 編碼的金鑰值,並輸出已簽署的網址。您可以將該網址用於特定網址的 GETHEAD 要求。

例如:

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 查詢參數。到期時間以世界標準時間為準,並為從 1970-01-01 00:00:00 UTC 起算的秒數。為了儘可能提升安全性,請根據您的用途將這個值設為可能的最短時間範圍。已簽署網址的有效期限越長,取得網址的使用者與其他人分享這個網址的風險就越高,無論是不小心或其他原因都有可能。

  3. 設定金鑰名稱。網址必須使用提供網址的後端服務或後端值區金鑰進行簽署。最好使用最近新增的金鑰進行金鑰輪替。在網址後方加上 &KeyName=KEY_NAME,即可將金鑰新增至網址。將 KEY_NAME 替換為在「建立已簽署的要求金鑰」一文中建立的所選金鑰名稱。

  4. 簽署網址。請按照下列步驟建立已簽署的網址。請確認查詢參數的順序與步驟 1 中立即顯示的順序相同,並確認簽署網址中的內容不會變更大小寫。

    a. 使用與先前選擇的金鑰名稱相對應的密鑰,透過 HMAC-SHA1 雜湊處理整個網址 (包含開頭的 http://https:// 以及結尾的 &KeyName...)。請使用原始的 16 個位元組密鑰,而不是 base64url 編碼金鑰。如有需要,請將金鑰解碼。

    b. 使用 base64url encode 編碼結果。

    c. 將 &Signature= 附加至網址,後面接著已編碼的簽名。請勿將簽章結尾的 = 字元轉換為百分比編碼格式 %3D

使用已簽署網址的網址前置字串

您可以只簽署 URLPrefixExpiresKeyName 查詢參數,而非使用 ExpiresKeyName 查詢參數簽署完整要求網址。這可讓特定的 URLPrefixExpiresKeyNameSignature 查詢參數組合,在與 URLPrefix 相符的多個網址中重複使用,避免為每個獨特網址建立新的簽章。

在下列範例中,醒目顯示的文字會顯示您簽署的參數。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 命名的密鑰,獨立產生下列網址的簽章:

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 解碼值是 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 身分與存取權管理角色)。

停用值區的公開存取權之後,個別使用者如果具備存取權限 (例如 OWNER 權限),便可繼續存取 Cloud Storage,而不需要已簽署的網址。

如要移除 Cloud Storage 值區的公開 allUsers READ 存取權,請反向操作將值區中的所有物件設為可供公開讀取中所述的動作。

發布及使用已簽署的網址

從 Google Cloud CLI 傳回或是您透過自訂程式碼產生的網址可依據您的需求發布。建議您只簽署 HTTPS 網址,因為 HTTPS 提供安全的傳輸機制,可防止已簽署網址的 Signature 元件遭到攔截。同理,請務必透過安全傳輸通訊協定 (例如 TLS/HTTPS) 發布已簽署的網址。