署名付き Cookie を使用する

このページでは、署名付き Cookie の概要と、Cloud CDN で署名付き Cookie を使用する手順を説明します。署名付き Cookie は、ユーザーが Google アカウントを持っているかどうかに関係なく、期限付きで一連のファイルにアクセスできるようにするための手段です。

署名付き Cookie は、署名付き URL の代わりとなる手段です。アプリケーションでユーザーごとに数十、数百の URL に個別に署名するのが不可能な場合は、署名付き Cookie によってアクセスを保護できます。

署名付き Cookie で、次のことが可能になります。

  • ユーザーを承認し、(各 URL に署名する代わりに)保護されたコンテンツへのアクセス用の期限付きトークンを提供する。
  • ユーザーがアクセスできる範囲を特定の URL 接頭辞(https://media.example.com/videos/ など)に限定し、その URL 接頭辞のみを範囲として、承認済みユーザーに保護されたコンテンツへのアクセス権を付与する。
  • URL とメディア マニフェストを変更せずに、パッケージング パイプラインを簡素化し、キャッシュ可能性を高める。

上記とは異なり、アクセス範囲を特定の URL に限定する場合は、署名付き URL の使用を検討してください。

始める前に

署名付き Cookie を使用する前に、次のことを行います。

  • Cloud CDN が有効になっていることを確認します。手順については、Cloud CDN の使用をご覧ください。Cloud CDN を有効にする前にバックエンドで署名付き Cookie を構成できますが、Cloud CDN が有効になるまでその効果はありません。

  • 必要に応じて、次のコマンドで Google Cloud CLI を最新バージョンに更新します。

    gcloud components update
    

概要については、署名付き URL と署名付き Cookie をご覧ください。

署名付きリクエストの鍵の構成

署名付き URL または署名付き Cookie の鍵を作成するには、いくつかの手順が必要です。以降のセクションで、必要な手順を説明します。

セキュリティ上の考慮事項

次のような場合、Cloud CDN はリクエストを検証しません。

  • リクエストが署名されていない。
  • リクエストのバックエンド サービスまたはバックエンド バケットで Cloud CDN が有効になっていない。

レスポンスを配信する前に、署名付きリクエストを必ず送信元で検証する必要があります。これは、署名されたコンテンツと署名されていないコンテンツの両方を配信するために送信元が使用される可能性があり、また、クライアントが送信元に直接アクセスする可能性があるためです。

  • Cloud CDN は、Signature クエリ パラメータまたは Cloud-CDN-Cookie HTTP Cookie がないリクエストをブロックしません。無効(または不正)なリクエスト パラメータを含むリクエストは拒否されます。
  • アプリケーションが無効な署名を検出した場合は、必ずアプリケーションが HTTP 403 (Unauthorized) レスポンス コードを使って応答するようにしてください。HTTP 403 レスポンス コードはキャッシュに保存できません。
  • 署名付きリクエストと署名がないリクエストに対するレスポンスは別々にキャッシュされるため、有効な署名付きリクエストへの正常なレスポンスは、署名のないリクエストの対応で使用されません。
  • アプリケーションがキャッシュ可能なレスポンス コードを無効なリクエストに送信すると、今後、有効なリクエストが誤って拒否される可能性があります。

Cloud Storage バックエンドの場合は、有効な署名がないリクエストを Cloud Storage が拒否できるよう公開アクセス権を削除する必要があります。

次の表に動作をまとめます。

リクエストに署名があるか キャッシュ ヒット 動作
いいえ いいえ バックエンドの配信元に転送します。
いいえ はい キャッシュから配信します。
はい いいえ 署名を検証します。有効な場合、バックエンドの配信元に転送します。
はい はい 署名を検証します。有効であればキャッシュから提供します。

署名付きリクエスト鍵を作成する

Cloud CDN の署名付き URL と署名付き Cookie のサポートを有効にするには、Cloud CDN が有効化されたバックエンド サービスバックエンド バケット、またはその両方で 1 つ以上の鍵を作成します。

それぞれのバックエンド サービスまたはバックエンド バケットで、セキュリティのニーズに応じて鍵を作成、削除できます。1 つのバックエンドにつき、構成できる鍵は最大 3 つです。鍵を定期的にローテーションすることをおすすめします。そうするには、最も古い鍵を削除してから新しい鍵を追加し、その鍵で URL または Cookie に署名します。

バックエンドごとの鍵のセットは互いに独立しているので、複数のバックエンド サービスやバックエンド バケットで同じ鍵名を使用きます。鍵名の長さは最大 63 文字です。鍵の名前には、A~Z、a~z、0~9、_(アンダースコア)、-(ハイフン)を使用します。

鍵を作成するときは、外部に漏れないように注意してください。鍵を知っている人は誰でも、その鍵が Cloud CDN から削除されるまでは、Cloud CDN が受け入れる署名付き URL または署名付き Cookie を作成できるからです。鍵は、署名付き URL または署名付き Cookie の生成に使用された PC に保存されます。また Cloud CDN も、リクエストの署名を検証するために鍵を保存します。

鍵を非公開にする目的で、API リクエストへのレスポンスには鍵の値が含まれません。鍵を紛失してしまった場合は、新規作成する必要があります。

署名付きリクエスト鍵を作成する手順は次のとおりです。

コンソール

  1. Google Cloud コンソールで、[Cloud CDN] ページに移動します。

    Cloud CDN に移動

  2. キーを追加する送信元の名前をクリックします。
  3. [送信元の詳細] ページで、[編集] ボタンをクリックします。
  4. [Origin basics] セクションで、[次へ] をクリックして [Host and path rules] セクションを開きます。
  5. [Host and path rules] セクションで、[次へ] をクリックして [Cache performance] セクションを開きます。
  6. [制限付きコンテンツ] セクションで、[署名付き URL と署名付き Cookie を使用してアクセスを制限する] を選択します。
  7. [署名鍵を追加] をクリックします。

    1. 新しい署名鍵の一意の名前を指定します。
    2. [キーの作成方法] セクションで、[自動的に生成する] を選択します。または、[入力する] をクリックして、署名鍵の値を指定します。

      前者の場合は、自動生成された署名鍵の値をプライベート ファイルにコピーします。これは、署名付き URL の作成に使用できます。

    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 の権限を構成する

Cloud Storage を使用し、オブジェクトを読み取れるユーザーを限定している場合は、Cloud CDN にオブジェクトの読み取りアクセス権を付与する必要があります。それには、Cloud CDN サービス アカウントを Cloud Storage ACL に追加します。

サービス アカウントの作成は必要ありません。サービス アカウントは、プロジェクト内のバックエンド バケットに初めて鍵を追加するときに自動的に作成されます。

次のコマンドを実行する前に、少なくとも 1 つの鍵をプロジェクトのバックエンド バケットに追加します。こうしないと、このコマンドはエラーを伴って失敗します。これは、プロジェクト用に 1 つ以上の鍵を追加するまでは、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 コンソールのヘルプ ドキュメントのプロジェクト ID とプロジェクト番号の特定をご覧ください。

最大キャッシュ時間をカスタマイズする

Cloud CDN は、バックエンドの Cache-Control ヘッダーとは無関係に、署名付きリクエストのレスポンスをキャッシュに保存します。再検証を必要とすることなくキャッシュに保存できる時間の最大値を設定するには、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

署名付きリクエスト鍵を削除する

特定の鍵で署名されている URL が不要になった場合は、次のコマンドのいずれかを実行して、その鍵をバックエンド サービスまたはバックエンド バケットから削除します。

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 のポリシーは、署名付き URL のクエリ パラメータに似た(: 文字で区切られた)一連の key-value ペアです。例については、ユーザーへの Cookie の発行をご覧ください。

ポリシーは、リクエストを有効なものとするためのパラメータを表します。ポリシーには、ハッシュベースのメッセージ認証コード(HMAC)を使用して署名されます。Cloud CDN は各リクエストでこれを検証します。

ポリシーの形式とフィールドを定義する

次の 4 つの必須フィールドについて、次の順序で定義する必要があります。

  • URLPrefix
  • Expires
  • KeyName
  • Signature

署名付き Cookie のポリシーで使用される key-value ペアでは大文字と小文字が区別されます。

URLPrefix

URLPrefix は、URL セーフな base64 でエンコードされた URL 接頭辞を示します。これは、署名が有効になるすべてのパスを網羅します。

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

リクエストされた URL が URLPrefix と一致しない場合、Cloud CDN はリクエストを拒否し、クライアントに HTTP 403 エラーを返します。

Expires

Expires は、Unix タイムスタンプ(1970 年 1 月 1 日からの秒数)である必要があります。

KeyName

KeyName は、バックエンド バケットまたはバックエンド サービスに対して作成された鍵の名前です。鍵名では大文字と小文字が区別されます。

Signature

Signature は、URL セーフな Base64 でエンコードされた、Cookie ポリシーを構成するフィールドの 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
}

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

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.

    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.utcfromtimestamp(0)
    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 Signer を作成します。

  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 に含まれる Domain 属性と Path 属性により、クライアントがその Cookie を Cloud CDN に送信するかどうかが決まります。

推奨事項と要件

  • 保護されたコンテンツの配信元となるドメインとパス接頭辞に一致する Domain 属性と Path 属性を明示的に設定します。これは、Cookie が発行されるドメインおよびパスと異なる可能性があります(example.commedia.example.com/browse/videos)。

  • 同じ DomainPath に対しては、所定の名前の Cookie を必ず 1 つだけ使用してください。

  • 競合する Cookie を発行していないことを確認してください。Cookie が競合すると、他のブラウザ セッション(ウィンドウまたはタブ)でコンテンツにアクセスできなくなる可能性があります。

  • 必要に応じて Secure フラグと HttpOnly フラグを設定します。Secure を設定すると、Cookie が HTTPS 接続でのみ送信されます。HttpOnly を設定すると、JavaScript で Cookie が使用不可になります。

  • Cookie 属性の ExpiresMax-Age は省略可能です。これらを省略すると、Cookie はブラウザ セッション(タブ、ウィンドウ)が終了するまで存続します。

  • キャッシュ フィルまたはキャッシュミスが発生すると、署名付き Cookie は、バックエンド サービスで定義された送信元に渡されます。コンテンツを配信する前に、各リクエストで署名付き Cookie の値を必ず検証してください。