署名付き URL を使用する

このページでは、署名付き URL の概要と、Cloud CDN で署名付き URL を使用する手順を説明します。署名付き URL は、ユーザーが Google アカウントを持っているかどうかにかかわらず、その URL を知っている全員にリソースへのアクセスを期間限定で許可する手段です。

署名付き URL は、リクエスト時における制限付きの権限と有効期限が設定された URL です。署名付き URL のクエリ文字列には認証情報が含まれているので、認証情報を持たないユーザーでもリソースに対して特定の操作を実行できます。署名付き URL を生成するときには、その URL に関連付けられたリクエストを行うのに十分な権限を持つユーザーまたはサービス アカウントを指定します。

署名付き URL を生成すると、その URL を所有するすべての人は、指定された期間内にその署名付き URL を使って特定のアクション(オブジェクトの読み取りなど)を実行できます。

また署名付き URL ではオプションの URLPrefix パラメータを使用して、共通の接頭辞に基づき複数の URL へのアクセスを許可することもできます。

アクセスを特定の URL 接頭辞に限定する場合は、署名付き Cookie の使用を検討してください。

始める前に

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

  • Cloud CDN が有効になっていることを確認します。手順については、Cloud CDN の使用をご覧ください。Cloud CDN を有効にする前にバックエンドで署名付き URL を構成できますが、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 キャッシュ フィル サービス アカウントが作成されないためです。

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 とプロジェクト番号の特定をご覧ください。

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

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

署名付き URL

最後のステップは、URL に署名を付加して配布することです。URL に署名を付加するには、gcloud compute sign-url コマンドを使用するか、独自のコードを記述します。多数の署名付き URL が必要な場合は、カスタムコードを使用したほうがパフォーマンスが向上します。

署名付き URL を作成する

gcloud compute sign-url コマンドを使用して、署名付き URL を作成する手順は次のとおりです。この手順では、鍵の作成がすでに完了していることを前提としています。

コンソール

Google Cloud コンソールを使用して署名付き URL を作成することはできません。Google Cloud CLI を使用するか、次の例を使用してカスタムコードを作成できます。

gcloud

Google Cloud CLI には、URL に署名するためのコマンドがあります。このコマンドは、独自のコードの作成に関するセクションで説明されているアルゴリズムを実装します。

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

このコマンドは、base64url でエンコードされた鍵の値を KEY_FILE_NAME から読み取ってデコードしてから、署名付き URL を出力します。この署名付き URL は、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 は、パス要素を持つ有効な URL でなければなりません。たとえば、http://example.com は無効ですが、https://example.com/https://example.com/whatever はどちらも有効な URL です。

省略可能な --validate フラグがこのコマンドで指定されている場合は、生成された URL とともに HEAD リクエストが送信され、HTTP レスポンス コードが出力されます。署名付き URL が正しい場合は、レスポンス コードと、バックエンドから送信された結果コードが同じになります。レスポンス コードが同じでない場合は、KEY_NAME と指定したファイルの内容を再確認し、TIME_UNTIL_EXPIRATION の値が数秒以上であることを確認してください。

--validate フラグが指定されていない場合、以下のものは検証されません。

  • 入力
  • 生成された URL
  • 生成された署名付き URL

署名付き URL をプログラムで作成する

次のサンプルコードは、署名付き URL をプログラムで作成する方法を示しています。

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}";
}

URL 接頭辞を含む署名付き URL をプログラマティックに作成する

次のコードサンプルは、URL 接頭辞を含む署名付き URL をプログラマティックに作成する方法を示しています。

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}"

カスタム署名付き URL を生成する

署名付き URL を生成するコードを独自に作成する場合、次の形式またはアルゴリズムで URL を作成することを目標にしてください。すべての URL パラメータは大文字と小文字が区別され、次に示す順序で設定される必要があります。

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

署名付き URL を生成する方法は次のとおりです。

  1. 署名対象の URL に Signature クエリ パラメータがないことを確認します。

  2. この URL の有効期限を決めて、Expires クエリ パラメータを追加します。有効期限を UTC 時間(1970-01-01 00:00:00 UTC からの秒数)として指定してください。安全性を最大にするために、ユースケースに応じて可能な限り短い有効期間となるように値を設定してください。署名付き URL の有効期間が長くなるほど、偶発的かどうかを問わず、他のユーザーに URL が渡ってしまうリスクが高くなります。

  3. 鍵の名前を設定します。URL に署名するには、URL を処理するバックエンド サービスまたはバックエンド バケットの鍵を使用する必要があります。最近追加した最新の鍵を使用して鍵をローテーションすることをおすすめします。&KeyName=KEY_NAME を追加して、鍵を URL に追加します。KEY_NAME は、署名済みのリクエスト鍵の作成で作成した鍵を選び、その名前に置き換えます。

  4. URL に署名します。次の手順で署名付き URL を作成します。クエリ パラメータを指定する順序が、ステップ 1 の直前で説明したとおりであることと、大文字と小文字の区別も説明のとおりであることを確認してください。

    a. URL 全体(先頭の http:// または https:// と末尾の &KeyName... も含む)を HMAC-SHA1 でハッシュします。このときに、上で選択した鍵名に対応する秘密鍵を使用します。base64url エンコードされた鍵ではなく、16 バイトの秘密鍵をそのまま使用します。必要に応じてデコードしてください。

    b. base64url エンコードを使用して結果をエンコードします。

    c. &Signature= を URL の末尾に付加し、その後にエンコード済みの署名を付加します。

署名付き URL に URL 接頭辞を使用する

クエリ パラメータ Expires および KeyName を含むリクエスト URL 全体を署名する代わりに、クエリ パラメータ URLPrefixExpiresKeyName だけに署名を付けることもできます。これにより、URLPrefix と一致する複数の URL でクエリ パラメータ URLPrefixExpiresKeyNameSignature の所定の組み合わせをそのまま再利用できるので、個別の URL ごとに新しい署名を作成する必要がなくなります。

次の例でハイライト表示されたテキストは、署名されるパラメータを示しています。Signature は、通常どおり、最後のクエリ パラメータとして付加します。

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

完全なリクエスト URL に署名する場合と異なり、URLPrefix で署名する場合はクエリ パラメータに署名することにはならないため、URL にクエリ パラメータを自由に含めることができます。また、完全なリクエスト URL 署名とは異なり、これらの追加のクエリ パラメータは、署名を構成するクエリ パラメータの前後の両方で指定できます。そのため、次の例も、署名付き URL 接頭辞を含む有効な URL です。

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

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 エラーを返します。

署名付き URL を検証する

署名付き URL を検証するプロセスは、署名付き URL の生成プロセスと基本的に同じです。たとえば、次の署名付き URL を検証するとします。

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

KEY_NAME という名前の秘密鍵を使用して、次の URL の署名を個別に生成できます。

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

その後、SIGNATURE と一致することを確認できます。

URLPrefix を含む署名付き URL を検証する場合、次のようにします。

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

まず、base64 でデコードされた URL_PREFIX の値が https://example.com/PATH の接頭辞であることを確認します。その場合、次の署名を計算できます。

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

その後、SIGNATURE と一致することを確認できます。

URL ベースの署名メソッドの場合、署名がクエリ パラメータの一部であるか、URL パス コンポーネントとして埋め込まれている場合、署名と関連パラメータは、リクエストが送信元に送信される前に URL から削除されます。これにより、オリジンがリクエストを処理する際に署名がルーティングの問題を引き起こすことがなくなります。これらのリクエストを検証するには、x-client-request-url リクエスト ヘッダーを調べます。このヘッダーには、署名付きコンポーネントが削除される前の元の(署名付き)クライアント リクエスト URL が含まれています。

Cloud Storage バケットに対する公開アクセスを削除する

署名付き URL でコンテンツを適切に保護するために、送信元サーバーでそのコンテンツへの公開アクセスを許可しないことが重要です。Cloud Storage バケットを使用する場合、一般的な方法として、テスト用にオブジェクトを一時的に公開します。署名付き URL を有効にした後で、バケットに対する allUsers(および該当する場合は allAuthenticatedUsers)読み取り権限(言い換えると Storage オブジェクト閲覧者 Identity and Access Management ロール)を削除することが重要です。

バケットに対する公開アクセスを無効にした後でも、個々のユーザーにオーナー権限などのアクセス権がある場合、署名付き URL なしで Cloud Storage にアクセスできます。

Cloud Storage バケットに対する公開 allUsers 読み取りアクセス権を削除するには、バケット内のすべてのオブジェクトを公開するで説明されている操作を逆に行います。

署名付き URL を配布して使用する

必要に応じて、Google Cloud CLI から返された URL、またはカスタムコードによって生成された URL を配布できます。HTTPS URL だけに署名することをおすすめします。HTTPS はセキュリティに優れたトランスポート手段であり、署名付き URL の Signature コンポーネントが傍受されるのを防止できるからです。同様に、署名付き URL を配布するときは、TLS / HTTPS などのセキュアなトランスポート プロトコルを使用してください。