서명된 URL 사용

이 페이지에서는 서명된 URL의 개요와 Cloud CDN에서 서명된 쿠키를 사용하는 방법을 설명합니다. 서명된 URL은 사용자가 Google 계정을 가지고 있는지 여부에 관계없이 URL을 통해 모든 사용자에게 제한된 리소스 액세스 권한을 제공합니다.

서명된 URL은 요청을 수행하는 데 필요한 제한된 권한과 시간을 제공하는 URL입니다. 서명된 URL은 쿼리 문자열에 인증 정보가 포함되어 있어 사용자 인증 정보가 없는 사용자도 리소스에 대한 특정 작업을 수행할 수 있습니다. 서명된 URL을 생성할 때는 URL과 연결된 요청을 수행하기에 충분한 권한이 있어야 하는 사용자 또는 서비스 계정을 지정합니다.

서명된 URL을 생성하면 서명된 URL을 소유한 모든 사람이 이를 사용하여 지정된 기간 내에 객체 읽기와 같은 지정된 작업을 수행할 수 있습니다.

서명된 URL은 선택사항인 URLPrefix 매개변수도 지원하므로 공통 프리픽스를 기반으로 여러 URL에 대한 액세스를 제공할 수 있습니다.

특정 URL 프리픽스에 대한 액세스 범위를 지정하려면 서명된 쿠키를 사용하는 것이 좋습니다.

시작하기 전에

서명된 URL을 사용하기 전에 다음을 수행합니다.

  • Cloud CDN이 사용 설정되어 있는지 확인합니다. 자세한 내용은 Cloud CDN 사용을 참조하세요. Cloud CDN을 사용 설정하기 전에 백엔드에서 서명된 URL을 구성할 수 있습니다. 하지만 Cloud CDN을 사용 설정할 때까지 효과는 없습니다.

  • 필요한 경우 Google Cloud CLI를 최신 버전으로 업데이트합니다.

    gcloud components update
    

개요는 서명된 URL 및 서명된 쿠키 개요를 참조하세요.

서명된 요청 키 구성

서명된 URL 또는 서명된 쿠키의 키를 만들려면 다음 섹션에 설명된 여러 단계가 필요합니다.

보안 고려사항

Cloud CDN은 다음 상황에서 요청 유효성을 검사하지 않습니다.

  • 요청이 서명되지 않았습니다.
  • 요청의 백엔드 서비스나 백엔드 버킷에 Cloud CDN이 사용 설정되어 있지 않습니다.

서명된 요청은 응답을 제공하기 전에 항상 원본에서 검증되어야 합니다. 원본은 서명된 콘텐츠와 서명되지 않은 콘텐츠의 혼합을 제공하는 데 사용될 수 있고 클라이언트가 원본에 직접 액세스할 수도 있기 때문입니다.

  • Cloud CDN은 Signature 쿼리 매개변수 또는 Cloud-CDN-Cookie HTTP 쿠키가 없는 요청을 차단하지 않습니다. 유효하지 않거나 잘못된 형식의 요청 매개변수가 있는 요청을 거부합니다.
  • 애플리케이션이 잘못된 서명을 감지하면 HTTP 403 (Unauthorized) 응답 코드로 응답하는지 확인합니다. HTTP 403 응답 코드를 캐시할 수 없습니다.
  • 서명된 요청과 서명되지 않은 요청에 대한 응답은 별도로 캐시되므로 유효한 서명된 요청에 대한 성공적인 응답은 서명되지 않은 요청을 제공하는 데 사용되지 않습니다.
  • 애플리케이션이 캐시 가능한 응답 코드를 잘못된 요청에 보내면 유효한 향후 요청이 부당하게 거부될 수 있습니다.

Cloud Storage 백엔드의 경우 공개 액세스를 삭제해야 Cloud Storage에서 유효한 서명이 없는 요청을 거부할 수 있습니다.

다음 표에는 동작이 요약되어 있습니다.

요청에 서명이 있음 캐시 적중 동작
아니요 아니요 백엔드 원본으로 전달합니다.
아니요 캐시에서 제공합니다.
아니요 서명 유효성을 검사합니다. 유효하면 백엔드 원본으로 전달합니다.
서명 유효성을 검사합니다. 유효하면 캐시에서 제공합니다.

서명된 요청 키 만들기

Cloud CDN이 사용 설정된 백엔드 서비스, 백엔드 버킷 또는 둘 다에 하나 이상의 키를 만들어 Cloud CDN 서명된 URL 및 서명된 쿠키에 대한 지원을 사용 설정합니다.

각 백엔드 서비스 또는 백엔드 버킷에 대해 보안 요구 사항에 따라 키를 만들고 삭제할 수 있습니다. 각 백엔드에는 한 번에 키를 최대 3개까지 구성할 수 있습니다. 가장 오래된 키를 삭제하고 새 키를 추가하고 URL 또는 쿠키에 서명할 때 새 키를 사용하여 키를 주기적으로 순환하는 것이 좋습니다.

각 키 집합은 상호 독립적이므로 여러 백엔드 서비스와 백엔드 버킷에서 동일한 키 이름을 사용할 수 있습니다. 키 이름은 최대 63자까지 구성될 수 있습니다. 키 이름을 지정하려면 A~Z, a~z, 0~9, _(밑줄), -(하이픈) 문자를 사용합니다.

사용자 키 중 하나를 가진 사람이 Cloud CDN에서 키가 삭제될 때까지 Cloud CDN이 수락하는 서명된 URL 또는 서명된 쿠키를 만들 수 있으므로 키를 만들 때 보안에 주의해야 합니다. 키는 서명된 URL 또는 서명된 쿠키를 생성하는 컴퓨터에 저장됩니다. 또한 Cloud CDN은 요청 서명을 확인하기 위해 키를 저장합니다.

키를 비밀로 유지하기 위해 키 값은 API 요청에 대한 응답에 포함되지 않습니다. 키를 분실한 경우 새 키를 만들어야 합니다.

서명된 요청 키를 만들려면 다음 단계를 따르세요.

콘솔

  1. Google Cloud 콘솔에서 Cloud CDN 페이지로 이동합니다.

    Cloud CDN으로 이동

  2. 키를 추가할 원본 이름을 클릭합니다.
  3. 원본 세부정보 페이지에서 수정 버튼을 클릭합니다.
  4. 원본 기본사항 섹션에서 다음을 클릭하여 호스트 및 경로 규칙 섹션을 엽니다.
  5. 호스트 및 경로 규칙 섹션에서 다음을 클릭하여 캐시 성능 섹션을 엽니다.
  6. 제한된 콘텐츠 섹션에서 서명된 URL 및 서명된 쿠키를 사용하여 액세스 제한을 선택합니다.
  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 Storage ACL에 추가하여 객체를 읽을 수 있는 권한을 Cloud CDN에 부여해야 합니다.

서비스 계정을 만들 필요가 없습니다. 서비스 계정은 키를 프로젝트의 백엔드 버킷에 처음 추가할 때 자동으로 생성됩니다.

다음 명령어를 실행하기 전에 프로젝트의 백엔드 버킷에 키를 최소 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 Console 도움말 문서의 프로젝트 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에 서명하고 이를 배포하는 것입니다. gcloud compute sign-url 명령어를 사용하거나 직접 작성한 코드를 사용하여 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]

이 명령어는 KEY_FILE_NAME에서 base64url로 인코딩된 키 값을 읽고 디코딩한 후 지정된 URL의 GET 또는 HEAD 요청에 사용할 수 있는 서명된 URL을 출력합니다.

예를 들면 다음과 같습니다.

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

자바

/** 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
}

자바

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단계 바로 전에 표시된 순서대로 되어 있는지 확인하고, 서명된 URL에서 대/소문자가 변경된 부분이 없는지 확인합니다.

    a. 앞에서 선택한 키 이름에 해당되는 보안 비밀 키를 사용하여 HMAC-SHA1로 전체 URL(처음 부분에 http:// 또는 https://, 끝부분에 &KeyName... 포함)을 해시합니다. base64url로 인코딩된 키가 아닌 원시 16바이트 비밀 키를 사용합니다. 필요한 경우 키를 디코딩합니다.

    b. base64url 인코딩을 사용하여 결과를 인코딩합니다.

    c. &Signature=와 인코딩된 서명을 차례로 URL에 추가합니다.

서명된 URL에 URL 프리픽스 사용

ExpiresKeyName 쿼리 매개변수로 전체 요청 URL에 서명하는 대신 URLPrefix, Expires, KeyName 쿼리 매개변수에만 서명할 수 있습니다. 이렇게 하면 URLPrefix, Expires, KeyName, Signature 쿼리 매개변수의 조합을 URLPrefix와 일치하는 여러 URL에서 그대로 재사용할 수 있으므로 각 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 프리픽스가 있는 유효한 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

먼저 URL_PREFIX의 base64 디코딩 값이 https://example.com/PATH의 프리픽스인지 확인합니다. 이 경우 다음과 같은 서명을 계산할 수 있습니다.

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

그런 다음 SIGNATURE와 일치하는지 확인할 수 있습니다.

서명이 쿼리 매개변수의 일부이거나 URL 경로 구성요소로 포함된 URL 기반 서명 메서드의 경우 요청이 원본에 전송되기 전에 서명 및 관련 매개변수가 URL에서 삭제됩니다. 이렇게 하면 원본이 요청을 처리할 때 서명이 라우팅 문제를 일으키지 않도록 방지됩니다. 이러한 요청을 검증하기 위해서는 서명된 구성요소를 삭제하기 전 원본(서명된) 클라이언트 요청 URL이 포함된 x-client-request-url 요청 헤더를 조사할 수 있습니다.

Cloud Storage 버킷에 대한 공개 액세스 삭제

서명된 URL이 콘텐츠를 적절히 보호하도록 하려면 원본 서버가 콘텐츠에 대한 공개 액세스를 허용하면 안 됩니다. Cloud Storage 버킷을 사용할 때 테스트를 하기 위해 객체를 한시적으로 공개하는 경우가 흔히 있습니다. 서명된 URL을 사용 설정한 뒤에는 버킷에서 allUsers(해당하는 경우 allAuthenticatedUsers도 포함) 읽기 권한(스토리지 객체 뷰어 Identity and Access Management 역할)을 삭제하는 것이 중요합니다.

개별 사용자가 소유자 권한 등의 액세스 권한을 가지고 있다면 버킷에서 공개 액세스를 사용 중지한 뒤에도 서명된 URL 없이 Cloud Storage에 액세스할 수 있습니다.

Cloud Storage 버킷에서 공개 allUsers 읽기 액세스 권한을 삭제하려면 버킷의 모든 객체를 읽을 수 있도록 공개에 설명된 작업을 역순으로 실행하세요.

서명된 URL 배포 및 사용

Google Cloud CLI에서 반환되거나 커스텀 코드로 생성된 URL은 필요에 따라 배포할 수 있습니다. HTTPS는 서명된 URL의 Signature 구성요소를 가로채지 못하게 막는 안전한 전송을 제공하므로 HTTPS URL만 서명하는 것이 좋습니다. 마찬가지로 TLS/HTTPS와 같은 보안 전송 프로토콜을 통해 서명된 URL을 배포해야 합니다.