使用已簽署的 Cookie

本頁面概述簽署的 Cookie,並說明如何搭配 Cloud CDN 使用。已簽署的 Cookie 可讓使用者在一段時間內存取一組檔案,無論使用者是否擁有 Google 帳戶。

已簽署的 Cookie 是已簽署網址的替代方案。如果應用程式無法為每位使用者分別簽署數十或數百個網址,已簽署的 Cookie 就能保護存取權。

已簽署的 Cookie 可讓您執行下列操作:

  • 授權使用者並提供限時存取權權杖,讓他們存取受保護的內容 (不必為每個網址簽署)。
  • 將使用者的存取權範圍限定在特定網址前置字元 (例如 https://media.example.com/videos/),並只允許授權使用者存取該網址前置字元內的受保護內容。
  • 維持網址和媒體資訊清單不變,簡化封裝管道並提升快取能力。

如要將存取權範圍限定在特定網址,請考慮使用已簽署的網址

事前準備

使用已簽署的 Cookie 前,請先完成下列步驟:

  • 確認已啟用 Cloud CDN;如需操作說明,請參閱「使用 Cloud CDN」。啟用 Cloud CDN 前,您可以在後端設定簽署的 Cookie,但必須啟用 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. 在「來源基本資訊」部分,按一下「下一步」,開啟「主機與路徑規則」部分。
  5. 在「主機與路徑規則」部分中,按一下「下一步」,開啟「快取效能」部分。
  6. 在「受限制的內容」部分,選取「透過已簽署的網址和 Cookie 限制存取權限」
  7. 按一下「新增簽署金鑰」

    1. 為新的簽署金鑰指定專屬名稱。
    2. 在「金鑰建立方法」部分,選取「自動產生」。 或者,按一下「讓我輸入」,然後指定簽署金鑰值。

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

    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

如要將金鑰新增至後端 bucket:

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 都會快取已簽署要求的回應。在您為回應設定可快取時間長度上限標記之後,使用者不必重新驗證該網址即可加以快取。系統預設的可快取時間長度上限為 1 小時,不過您可以按照下列步驟修改這項設定。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

建立政策

已簽署的 Cookie 政策是一連串的 key-value 配對 (以 : 字元做為分隔符號),類似於已簽署網址中使用的查詢參數。請參閱「向使用者發放 Cookie」中的範例。

政策代表要求有效的參數。政策會使用雜湊架構訊息驗證碼 (HMAC) 簽署,Cloud CDN 會在每個要求中驗證這類政策。

定義政策格式和欄位

您必須依下列順序定義四個必填欄位:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

簽署的 Cookie 政策中的 key-value 配對會區分大小寫。

URLPrefix

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 錯誤。

有效期限

Expires 必須是 Unix 時間戳記 (自 1970 年 1 月 1 日起算,以秒為單位)。

KeyName

KeyName 是針對後端 bucket 或後端服務建立的金鑰名稱。金鑰名稱會區分大小寫。

簽名

Signature 是網址安全 Base64 編碼的 HMAC-SHA-1 簽章,由構成 Cookie 政策的欄位組成。系統會在每次要求時驗證這項資訊,如果簽章無效,系統會拒絕要求並傳回 HTTP 403 錯誤。

透過程式建立已簽署的 Cookie

下列程式碼範例示範如何以程式輔助方式建立已簽署的 Cookie。

Go

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

// signCookie creates a signed cookie for an endpoint served by Cloud CDN.
//
// - urlPrefix must start with "https://" and should include the path prefix
// for which the cookie will authorize access to.
// - 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 signCookie(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	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 SignedCookies {

  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 Unix timestamp that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // 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);

    // Create signed cookie from policy.
    String signedCookie = signCookie(urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signedCookie);
  }

  // Creates a signed cookie for the specified policy.
  public static String signCookie(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 policyToSign = String.format("URLPrefix=%s:Expires=%d:KeyName=%s", encodedUrlPrefix,
        expirationTime, keyName);

    String signature = getSignatureForUrl(key, policyToSign);
    return String.format("Cloud-CDN-Cookie=%s:Signature=%s", policyToSign, signature);
  }

  // Creates signature for input string 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_cookie(
    url_prefix: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed cookie value for the specified URL prefix and configuration.

    Args:
        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 Cloud-CDN-Cookie value based on the specified configuration.
    """
    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")

    signed_policy = f"Cloud-CDN-Cookie={policy}:Signature={signature}"

    return signed_policy

驗證已簽署的 Cookie

驗證已簽署 Cookie 的程序,基本上與產生已簽署 Cookie 的程序相同。舉例來說,假設您想驗證下列已簽署的 Cookie 標頭:

Cookie: Cloud-CDN-Cookie=URLPrefix=URL_PREFIX:Expires=EXPIRATION:KeyName=KEY_NAME:Signature=SIGNATURE; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly

您可以使用 KEY_NAME 指定的私密金鑰,獨立產生簽名,然後驗證簽名是否與 SIGNATURE 相符。

向使用者發放 Cookie

應用程式必須為每位使用者 (用戶端) 產生並發出單一 HTTP Cookie,其中包含正確簽署的政策:

  1. 在應用程式程式碼中建立 HMAC-SHA-1 簽署者。

  2. 使用所選金鑰簽署政策,並記下您新增至後端的金鑰名稱,例如 mySigningKey

  3. 建立下列格式的 Cookie 政策,請注意名稱和值都會區分大小寫:

    Name: Cloud-CDN-Cookie
    Value: URLPrefix=$BASE64URLECNODEDURLORPREFIX:Expires=$TIMESTAMP:KeyName=$KEYNAME:Signature=$BASE64URLENCODEDHMAC
    

    標頭範例:Set-Cookie

    Set-Cookie: Cloud-CDN-Cookie=URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv:Expires=1566268009:KeyName=mySigningKey:Signature=0W2xlMlQykL2TG59UZnnHzkxoaw=; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly
    

    Cookie 中的 DomainPath 屬性會決定用戶端是否將 Cookie 傳送至 Cloud CDN。

建議與需求條件

  • 明確設定 DomainPath 屬性,以符合您打算放送受保護內容的網域和路徑前置字元,這可能與發行 Cookie 的網域和路徑不同 (example.commedia.example.com/browse/videos)。

  • 請確認您在同一個 DomainPath 中,只有一個具有指定名稱的 Cookie。

  • 請確認您未發布衝突的 Cookie,否則可能會導致其他瀏覽器工作階段 (視窗或分頁) 無法存取內容。

  • 視情況設定 SecureHttpOnly 標記。Secure 可確保 Cookie 只能透過 HTTPS 連線傳送。HttpOnly 可防止 JavaScript 存取 Cookie。

  • Cookie 屬性 ExpiresMax-Age 為選用屬性。如果省略這些屬性,Cookie 會在瀏覽器工作階段 (分頁、視窗) 存在期間有效。

  • 在快取填補或快取未命中時,簽署的 Cookie 會傳遞至後端服務中定義的來源。請務必在每次要求時驗證已簽署的 Cookie 值,再放送內容。