使用签名 Cookie

本页面简要介绍签名 Cookie 以及将其用于 Cloud CDN 的说明。 无论用户是否拥有 Google 帐号,都可以通过签名 Cookie 获得对一组文件的限时资源访问权限。

签名 Cookie 是签名网址的替代方案。如果在应用中为每个用户单独签名数十乃至数百个网址不可行时,签名 Cookie 可以保障访问的安全。

通过签名 Cookie,您可以实现如下目标:

  • 向用户授权,并向他们提供用于访问受保护内容的限时令牌(而不必对每个网址进行签名)。
  • 将用户的访问权限限制在特定网址前缀(如 https://media.example.com/videos/)的范围内,并仅向该授权用户授予对该网址前缀内受保护内容的访问权限。
  • 保持您的网址和媒体清单不变,从而简化打包流水线并提高可缓存性。

如果您希望将访问权限限制在特定的网址范围内,请考虑使用签名网址

准备工作

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

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

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

    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 Console 中,转到 Cloud CDN 页面。

    转到 Cloud CDN 页面

  2. 点击添加源站
  3. 选择一个 HTTP(S) 负载平衡器作为源站。
  4. 选择后端服务后端存储分区。对于所选的每一项执行以下操作:
    1. 点击配置,然后点击添加签名密钥
    2. 名称下,为新的签名密钥命名。
    3. 密钥创建方法下,选择自动生成由我输入
    4. 如果您要输入自己的密钥,请在文本字段中输入密钥。
    5. 点击完成
    6. 缓存条目最长存在时间下提供一个值,然后从下拉列表中选择一个时间单位。您可以选择分钟小时。最长时间为三 (3) 天。
  5. 点击保存
  6. 点击添加

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 CDN 服务帐号添加到 Cloud Storage ACL 中。

您无需创建服务帐号。首次向一个项目中的后端存储分区添加密钥时,系统会自动创建服务帐号。

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

gsutil iam ch \
  serviceAccount:service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com:objectViewer \
  gs://BUCKET

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

如需详细了解项目编号,请参阅 Google Cloud Console 帮助文档中的查找项目 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

创建政策

签名 Cookie 政策就是一系列 key-value 对(由 : 字符分隔),类似于签名网址中使用的查询参数。如需查看示例,请参阅向用户签发 Cookie

政策表示请求对其有效的参数。使用基于哈希的消息身份验证代码 (HMAC) 对政策进行签名,Cloud CDN 会针对每条请求验证 HMAC。

定义政策格式和字段

您必须按以下顺序定义四个必填字段:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

签名 Cookie 政策中的 key-value 对区分大小写。

URLPrefix

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

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

Expires 必须是 Unix 时间戳(自 1970 年 1 月 1 日以来的秒数)。

KeyName

KeyName 是对后端存储分区或后端服务创建的密钥的密钥名称。密钥名称区分大小写。

Signature

Signature 是构成 Cookie 政策的各字段的签名,采用可在网址中安全使用的 base64 编码的 HMAC-SHA-1 签名格式。系统会对每条请求进行签名验证,将会拒绝带有无效签名的请求,并显示 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
}

// readKeyFile reads the base64url-encoded key file and decodes it.
func readKeyFile(path string) ([]byte, error) {
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read key file: %+v", err)
	}
	d := make([]byte, base64.URLEncoding.DecodedLen(len(b)))
	n, err := base64.URLEncoding.Decode(d, b)
	if err != nil {
		return nil, fmt.Errorf("failed to base64url decode: %+v", err)
	}
	return d[:n], nil
}

func generateSignedCookie(w io.Writer) error {
	// The path to a file containing the base64-encoded signing key
	keyPath := os.Getenv("KEY_PATH")

	// Note: consider using the GCP Secret Manager for managing access to your
	// signing key(s).
	key, err := readKeyFile(keyPath)
	if err != nil {
		return err
	}

	var (
		// domain and path should match the user-facing URL for accessing
		// content.
		domain     = "media.example.com"
		path       = "/segments/"
		keyName    = "my-key"
		expiration = time.Hour * 2
	)

	signedValue, err := signCookie(fmt.Sprintf("https://%s%s", domain,
		path), keyName, key, time.Now().Add(expiration))
	if err != nil {
		return err
	}

	// Use Go's http.Cookie type to construct a cookie.
	cookie := &http.Cookie{
		Name:   "Cloud-CDN-Cookie",
		Value:  signedValue,
		Path:   path, // Best practice: only send the cookie for paths it is valid for
		Domain: domain,
		MaxAge: int(expiration.Seconds()),
	}

	// We print this to stdout in this example. In a real application, use the
	// SetCookie method on a http.ResponseWriter to write the cookie to the
	// user.
	fmt.Fprintln(w, cookie)

	return nil
}

Python

def sign_cookie(url_prefix, key_name, base64_key, expiration_time):
    """Gets the Signed cookie value for the specified URL prefix and configuration.

    Args:
        url_prefix: URL prefix to sign as a string.
        key_name: name of the signing key as a string.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as a UTC datetime object.

    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.datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy_pattern = u'URLPrefix={encoded_url_prefix}:Expires={expires}:KeyName={key_name}'
    policy = policy_pattern.format(
            encoded_url_prefix=encoded_url_prefix,
            expires=expiration_timestamp,
            key_name=key_name)

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

    signed_policy = u'Cloud-CDN-Cookie={policy}:Signature={signature}'.format(
            policy=policy, signature=signature)
    print(signed_policy)

向用户签发 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 可确保仅通过 HTTPS 连接发送 Cookie。HttpOnly 会禁止 JavaScript 使用 Cookie。

  • Cookie 特性 ExpiresMax-Age 是可选的。如果您省略这些属性,则 Cookie 会在浏览器会话(标签页或窗口)存在期间保持存在。

  • 在缓存填充或缓存未命中时,签名 Cookie 会传递到后端服务中定义的源站。请确保首先验证每个请求的签名 Cookie 值,之后再传送内容。