Menggunakan URL bertanda tangan

Halaman ini berisi ringkasan tentang URL bertanda tangan dan petunjuk untuk menggunakannya dengan Cloud CDN. URL bertanda tangan memberikan akses resource dengan waktu yang terbatas kepada siapa pun yang memiliki URL tersebut, terlepas dari apakah pengguna memiliki Akun Google atau tidak.

URL bertanda tangan adalah URL yang memberikan izin berbatas waktu untuk membuat permintaan. URL bertanda tangan berisi informasi autentikasi dalam string kuerinya, yang memungkinkan pengguna tanpa kredensial melakukan tindakan tertentu pada resource. Saat membuat URL bertanda tangan, Anda menentukan pengguna atau akun layanan yang harus memiliki izin yang memadai untuk membuat permintaan yang terkait dengan URL tersebut.

Setelah membuat URL bertanda tangan, siapa saja yang memilikinya dapat menggunakan URL tersebut untuk melakukan tindakan tertentu, seperti membaca objek, dalam jangka waktu tertentu.

URL bertanda tangan juga mendukung parameter URLPrefix opsional, sehingga Anda dapat memberikan akses ke beberapa URL berdasarkan awalan umum.

Jika Anda ingin mencakup akses ke awalan URL tertentu, pertimbangkan untuk menggunakan cookie bertanda tangan.

Sebelum memulai

Sebelum Anda menggunakan URL bertanda tangan, lakukan hal-hal berikut:

  • Pastikan Cloud CDN diaktifkan. Untuk mengetahui petunjuknya, baca bagian Menggunakan Cloud CDN. Anda dapat mengonfigurasi URL bertanda tangan di backend sebelum mengaktifkan Cloud CDN, tetapi tidak akan ada efeknya hingga Cloud CDN diaktifkan.

  • Jika perlu, update ke Google Cloud CLI versi terbaru:

    gcloud components update
    

Untuk mengetahui ringkasannya, baca bagian URL bertanda tangan dan cookie bertanda tangan.

Mengonfigurasi kunci permintaan bertanda tangan

Proses membuat kunci untuk URL bertanda tangan atau cookie bertanda tangan memerlukan beberapa langkah, yang dijelaskan di bagian berikut.

Pertimbangan keamanan

Cloud CDN tidak akan memvalidasi permintaan dalam situasi berikut:

  • Permintaan tidak ditandatangani.
  • Layanan backend atau bucket backend untuk permintaan tidak mengaktifkan Cloud CDN.

Permintaan bertanda tangan harus selalu divalidasi di server asal sebelum menyajikan respons. Hal ini harus dilakukan karena server awal dapat digunakan untuk menyajikan campuran konten bertanda tangan dan tidak bertanda tangan, dan karena klien dapat mengakses server tersebut secara langsung.

  • Cloud CDN tidak akan memblokir permintaan tanpa parameter kueri Signature atau cookie HTTP Cloud-CDN-Cookie. Cloud CDN akan menolak permintaan dengan parameter permintaan yang tidak valid (atau salah format).
  • Saat aplikasi Anda mendeteksi tanda tangan yang tidak valid, pastikan aplikasi Anda merespons dengan kode respons HTTP 403 (Unauthorized). Kode respons HTTP 403 tidak dapat di-cache.
  • Respons terhadap permintaan bertanda tangan dan tidak bertanda tangan akan di-cache secara terpisah, sehingga respons yang berhasil terhadap permintaan bertanda tangan yang valid tidak akan pernah digunakan untuk menyajikan permintaan yang tidak bertanda tangan.
  • Jika aplikasi Anda mengirimkan kode respons yang dapat di-cache ke permintaan yang tidak valid, permintaan mendatang yang valid mungkin akan ditolak secara keliru.

Untuk backend Cloud Storage, pastikan untuk menghapus akses publik, sehingga Cloud Storage dapat menolak permintaan yang tidak memiliki tanda tangan yang valid.

Tabel berikut merangkum perilaku tersebut.

Permintaan memiliki tanda tangan Cache ditemukan Perilaku
Tidak Tidak Meneruskan ke server asal backend.
Tidak Ya Menyajikan dari cache.
Ya Tidak Memvalidasi tanda tangan. Jika valid, meneruskan ke server asal backend.
Ya Ya Memvalidasi tanda tangan. Jika valid, menyajikan dari cache.

Membuat kunci permintaan bertanda tangan

Anda akan mengaktifkan dukungan untuk URL dan cookie bertanda tangan Cloud CDN dengan membuat satu atau beberapa kunci di layanan backend dengan Cloud CDN yang diaktifkan, bucket backend, atau keduanya.

Untuk tiap layanan backend atau bucket backend, Anda dapat membuat dan menghapus kunci sesuai kebutuhan keamanan Anda. Tiap backend dapat memiliki hingga tiga kunci yang dikonfigurasi sekaligus. Sebaiknya Anda merotasi kunci secara berkala dengan menghapus kunci terlama, menambahkan kunci baru, dan menggunakan kunci baru tersebut saat menandatangani URL atau cookie.

Anda dapat menggunakan nama kunci yang sama di beberapa layanan backend dan bucket backend karena tiap set kunci tidak bergantung pada yang lain. Nama kunci dapat berisi maksimal 63 karakter. Untuk memberi nama kunci, gunakan karakter A-Z, a-z, 0-9, _ (garis bawah), dan - (tanda hubung).

Saat membuat kunci, pastikan untuk menjaganya tetap aman karena siapa pun yang memiliki salah satu kunci Anda dapat membuat URL bertanda tangan atau cookie bertanda tangan yang divalidasi Cloud CDN hingga kunci tersebut dihapus dari Cloud CDN. Kunci akan disimpan di komputer tempat Anda membuat URL bertanda tangan atau cookie bertanda tangan. Cloud CDN juga akan menyimpan kunci untuk memverifikasi tanda tangan permintaan.

Untuk menjaga kerahasiaan kunci, nilai kunci tidak akan disertakan dalam respons terhadap permintaan API apa pun. Jika Anda kehilangan kunci, Anda harus membuat yang baru.

Untuk membuat kunci permintaan bertanda tangan, ikuti langkah-langkah berikut.

Konsol

  1. Di konsol Google Cloud , buka halaman Cloud CDN.

    Buka Cloud CDN

  2. Klik nama server asal yang ingin Anda tambahi kunci.
  3. Di halaman Origin details, klik tombol Edit.
  4. Di bagian Origin basics, klik Next untuk membuka bagian Host and path rules.
  5. Di bagian Host and path rules, klik Next untuk membuka bagian Cache performance.
  6. Di bagian Restricted content, pilih Restrict access using signed URLs and signed cookies.
  7. Klik Add signing key.

    1. Tentukan nama unik untuk kunci penandatanganan baru.
    2. Di bagian Key creation method, pilih Automatically generate. Atau, klik Let me enter, lalu tentukan nilai kunci penandatanganan.

      Untuk opsi pertama, salin nilai kunci penandatanganan yang dibuat secara otomatis ke file pribadi, yang dapat Anda gunakan untuk membuat URL bertanda tangan.

    3. Klik Done.

    4. Di bagian Cache entry maximum age, masukkan nilai, lalu pilih satuan waktu.

  8. Klik Done.

gcloud

Alat command line gcloud akan membaca kunci dari file lokal yang Anda tentukan. File kunci harus dibuat dengan membuat 128 bit acak yang kuat, mengenkode bit tersebut dengan base64, lalu mengganti karakter + dengan - dan mengganti karakter / dengan _. Untuk mengetahui informasi selengkapnya, baca bagian RFC 7300. Kunci harus sangat acak. Pada sistem mirip UNIX, Anda dapat membuat kunci acak yang kuat dan menyimpannya dalam file kunci dengan perintah berikut:

head -c 16 /dev/urandom | base64 | tr +/ -_ > KEY_FILE_NAME

Untuk menambahkan kunci ke layanan backend:

gcloud compute backend-services \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Untuk menambahkan kunci ke bucket backend:

gcloud compute backend-buckets \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Mengonfigurasi izin Cloud Storage

Jika Anda menggunakan Cloud Storage dan telah membatasi siapa yang dapat membaca objek, Anda harus memberikan izin kepada Cloud CDN untuk membaca objek dengan menambahkan akun layanan Cloud CDN ke ACL Cloud Storage.

Anda tidak perlu membuat akun layanan. Akun layanan dibuat secara otomatis saat Anda pertama kali menambahkan kunci ke bucket backend dalam project.

Sebelum menjalankan perintah berikut, tambahkan minimal satu kunci ke bucket backend di project Anda. Jika tidak, perintah akan gagal disertai error karena akun layanan pengisian cache Cloud CDN tidak akan dibuat sampai Anda menambahkan satu atau beberapa kunci untuk project tersebut.

gcloud storage buckets add-iam-policy-binding gs://BUCKET \
  --member=serviceAccount:service-PROJECT_NUMBER@cloud-cdn-fill.iam.gserviceaccount.com \
  --role=roles/storage.objectViewer

Ganti PROJECT_NUMBER dengan nomor project Anda dan BUCKET dengan bucket penyimpanan Anda.

Akun layanan Cloud CDN service-PROJECT_NUMBER@cloud-cdn-fill.iam.gserviceaccount.com tidak akan muncul dalam daftar akun layanan di project Anda. Hal ini karena akun layanan Cloud CDN dimiliki oleh Cloud CDN, bukan project Anda.

Untuk mengetahui informasi selengkapnya tentang nomor project, baca bagian Menemukan project ID dan nomor project dalam dokumentasi Bantuan konsol Google Cloud .

Menyesuaikan waktu cache maksimum

Cloud CDN menyimpan respons dalam cache untuk permintaan bertanda tangan, terlepas dari header Cache-Control backend. Waktu maksimum saat respons dapat di-cache tanpa validasi ulang ditetapkan oleh flag signed-url-cache-max-age, yang secara default adalah satu jam dan dapat diubah seperti yang ditunjukkan di sini.

Untuk menetapkan waktu cache maksimum untuk layanan backend atau bucket backend, jalankan salah satu perintah berikut:

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

Mencantumkan nama kunci permintaan bertanda tangan

Untuk mencantumkan kunci pada layanan backend atau bucket backend, jalankan salah satu perintah berikut:

gcloud compute backend-services describe BACKEND_NAME
gcloud compute backend-buckets describe BACKEND_NAME

Menghapus kunci permintaan bertanda tangan

Jika URL bertanda tangan oleh kunci tertentu tidak boleh lagi digunakan, jalankan salah satu perintah berikut untuk menghapus kunci tersebut dari layanan backend atau bucket backend:

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 tanda tangan

Langkah terakhir adalah menandatangani URL dan mendistribusikannya. Anda dapat menandatangani URL menggunakan perintah gcloud compute sign-url atau menggunakan kode yang Anda tulis sendiri. Jika Anda memerlukan banyak URL bertanda tangan, kode kustom akan memberikan performa yang lebih baik.

Membuat URL bertanda tangan

Gunakan petunjuk ini untuk membuat URL bertanda tangan menggunakan perintah gcloud compute sign-url. Langkah ini mengasumsikan bahwa Anda telah membuat kunci.

Konsol

Anda tidak dapat membuat URL bertanda tangan menggunakan konsol Google Cloud . Anda dapat menggunakan Google Cloud CLI atau menulis kode kustom menggunakan contoh berikut.

gcloud

Google Cloud CLI menyertakan perintah untuk menandatangani URL. Perintah ini mengimplementasikan algoritma yang dijelaskan di bagian menulis kode Anda sendiri.

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

Perintah ini membaca dan mendekode nilai kunci yang dienkode base64url dari KEY_FILE_NAME, lalu menampilkan URL bertanda tangan yang dapat Anda gunakan untuk permintaan GET atau HEAD untuk URL tertentu.

Contoh:

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 harus berupa URL valid yang memiliki komponen jalur. Misalnya, http://example.com tidak valid, tetapi https://example.com/ dan https://example.com/whatever adalah URL yang valid.

Jika flag --validate opsional diberikan, perintah ini akan mengirim permintaan HEAD dengan URL yang dihasilkan, dan mencetak kode respons HTTP. Jika URL bertanda tangan benar, kode respons akan sama dengan kode hasil yang dikirim oleh backend Anda. Jika kode respons tidak sama, periksa ulang KEY_NAME dan konten file yang ditentukan, serta pastikan nilai TIME_UNTIL_EXPIRATION adalah minimal beberapa detik.

Jika flag --validate tidak diberikan, hal-hal berikut tidak akan diverifikasi:

  • Input
  • URL yang dibuat
  • URL bertanda tangan yang dibuat

Membuat URL bertanda tangan secara terprogram

Contoh kode berikut menunjukkan cara membuat URL bertanda tangan secara terprogram.

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

Membuat URL bertanda tangan secara terprogram dengan awalan URL

Contoh kode berikut menunjukkan cara membuat URL bertanda tangan secara terprogram dengan awalan 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}"

Membuat URL bertanda tangan kustom

Saat menulis kode Anda sendiri untuk membuat URL bertanda tangan, sasaran Anda adalah membuat URL dengan format atau algoritma berikut; semua parameter URL peka huruf besar/kecil dan harus berada dalam urutan yang ditunjukkan:

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

Untuk membuat URL bertanda tangan, ikuti langkah-langkah berikut:

  1. Pastikan URL untuk penandatanganan tidak memiliki parameter kueri Signature.

  2. Tentukan akhir masa berlaku URL dan tambahkan parameter kueri Expires dengan waktu habis masa berlaku yang diperlukan dalam waktu UTC (jumlah detik sejak 1970-01-01 00:00:00 UTC). Untuk memaksimalkan keamanan, tetapkan nilai ke jangka waktu sesingkat mungkin untuk kasus penggunaan Anda. Makin lama validitas URL bertanda tangan, makin besar risiko pengguna yang Anda beri URL tersebut membagikannya kepada orang lain, baik sengaja maupun tidak.

  3. Tetapkan nama kunci. URL harus ditandatangani dengan kunci layanan backend atau bucket backend yang menyajikan URL. Sebaiknya gunakan kunci yang baru saja ditambahkan untuk rotasi kunci. Tambahkan kunci ke URL dengan menambahkan &KeyName=KEY_NAME. Ganti KEY_NAME dengan nama kunci yang dipilih yang dibuat di bagian Membuat kunci permintaan bertanda tangan.

  4. Tandatangani URL. Buat URL bertanda tangan dengan mengikuti langkah-langkah berikut. Pastikan parameter kueri berada dalam urutan yang ditampilkan persis sebelum langkah 1, dan pastikan tidak ada huruf besar atau kecil yang berubah dalam URL bertanda tangan tersebut.

    a. Hash seluruh URL (termasuk http:// atau https:// di awal dan &KeyName... di akhir) dengan HMAC-SHA1 menggunakan kunci rahasia yang sesuai dengan nama kunci yang dipilih sebelumnya. Gunakan kunci rahasia 16 byte mentah, bukan kunci berenkode base64url. Dekode jika diperlukan.

    b. Gunakan base64url encode untuk mengenkode hasilnya.

    c. Tambahkan &Signature= ke URL, diikuti dengan tanda tangan yang dienkode. Jangan mengonversi karakter akhir = tanda tangan ke format yang berenkode persen, %3D.

Menggunakan awalan URL untuk URL bertanda tangan

Alih-alih menandatangani URL permintaan lengkap dengan parameter kueri Expires dan KeyName, Anda dapat menandatangani parameter kueri URLPrefix, Expires, dan KeyName saja. Dengan demikian, kombinasi tertentu dari parameter kueri URLPrefix, Expires, KeyName, dan Signature dapat digunakan kembali secara harfiah di beberapa URL yang cocok dengan URLPrefix, sehingga tidak perlu membuat tanda tangan baru untuk tiap URL yang berbeda.

Dalam contoh berikut, teks yang ditandai menunjukkan parameter yang Anda tandatangani. Signature ditambahkan sebagai parameter kueri terakhir, seperti biasa.

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

Tidak seperti menandatangani URL permintaan lengkap, saat Anda menandatangani dengan URLPrefix, Anda tidak akan menandatangani parameter kueri apa pun, sehingga parameter kueri dapat disertakan secara bebas dalam URL. Selain itu, tidak seperti tanda tangan URL permintaan lengkap, parameter kueri tambahan tersebut dapat muncul sebelum dan setelah parameter kueri yang membentuk tanda tangan. Jadi, hasil berikut juga merupakan URL yang valid dengan awalan URL bertanda tangan:

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

URLPrefix menunjukkan awalan URL berenkode base64 yang aman untuk URL yang mencakup semua jalur dengan tanda tangan yang valid.

URLPrefix mengenkode skema (http:// atau https://), FQDN, dan jalur opsional. Mengakhiri jalur dengan / bersifat opsional, tetapi direkomendasikan. Awalan tidak boleh menyertakan parameter kueri atau fragmen seperti ? atau #.

Misalnya, https://media.example.com/videos cocok dengan permintaan ke kedua URL berikut:

  • https://media.example.com/videos?video_id=138183&user_id=138138
  • https://media.example.com/videos/137138595?quality=low

Jalur awalan digunakan sebagai substring teks, bukan jalur direktori. Misalnya, awalan https://example.com/data akan memberikan akses ke kedua hal berikut:

  • /data/file1
  • /database

Untuk menghindari kesalahan ini, sebaiknya akhiri semua awalan dengan /, kecuali jika Anda sengaja memilih untuk mengakhiri awalan dengan nama file parsial seperti https://media.example.com/videos/123 untuk memberikan akses ke hal-hal berikut:

  • /videos/123_chunk1
  • /videos/123_chunk2
  • /videos/123_chunkN

Jika URL yang diminta tidak cocok dengan URLPrefix, Cloud CDN akan menolak permintaan dan menampilkan error HTTP 403 kepada klien.

Memvalidasi URL bertanda tangan

Proses validasi URL bertanda tangan pada dasarnya sama dengan proses pembuatan URL bertanda tangan. Misalnya, Anda ingin memvalidasi URL bertanda tangan berikut:

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

Anda dapat menggunakan kunci rahasia yang dinamai oleh KEY_NAME guna membuat tanda tangan secara independen untuk URL berikut:

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

Kemudian, Anda dapat memverifikasi bahwa tanda tangan tersebut cocok dengan SIGNATURE.

Misalkan Anda ingin memvalidasi URL bertanda tangan yang memiliki URLPrefix, seperti yang ditunjukkan di sini:

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

Pertama, pastikan nilai URL_PREFIX yang didekode base64 adalah awalan dari https://example.com/PATH. Jika benar, Anda dapat menghitung tanda tangan untuk hal berikut:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

Kemudian, Anda dapat memverifikasi bahwa tanda tangan tersebut cocok dengan SIGNATURE.

Untuk metode penandatanganan berbasis URL, dengan tanda tangan sebagai bagian dari parameter kueri atau disematkan sebagai komponen jalur URL, tanda tangan dan parameter terkait akan dihapus dari URL sebelum permintaan dikirim ke server asal. Tindakan ini akan mencegah tanda tangan menyebabkan masalah perutean saat server asal menangani permintaan. Untuk memvalidasi permintaan ini, Anda dapat memeriksa header permintaan x-client-request-url, yang mencakup URL permintaan klien asli (bertanda tangan) sebelum penghapusan komponen bertanda tangan tersebut.

Menghapus akses publik ke bucket Cloud Storage

Agar URL bertanda tangan dapat melindungi konten dengan benar, server asal tidak boleh memberikan akses publik ke konten tersebut. Saat menggunakan bucket Cloud Storage, pendekatan umum yang dilakukan adalah menjadikan objek publik untuk sementara waktu demi tujuan pengujian. Setelah mengaktifkan URL bertanda tangan, penting untuk menghapus izin BACA allUsers (dan allAuthenticatedUsers, jika berlaku) (dengan kata lain, peran Identity and Access Management dari Storage Object Viewer) di bucket.

Setelah Anda menonaktifkan akses publik di bucket, pengguna masing-masing tetap dapat mengakses Cloud Storage tanpa URL bertanda tangan jika mereka memiliki izin akses, seperti izin PEMILIK.

Untuk menghapus akses BACA publik allUsers pada bucket Cloud Storage, batalkan tindakan yang dijelaskan dalam bagian Membuat semua objek dalam bucket dapat dibaca oleh publik.

Mendistribusikan dan menggunakan URL bertanda tangan

URL yang ditampilkan dari Google Cloud CLI atau dihasilkan oleh kode kustom Anda dapat didistribusikan sesuai kebutuhan Anda. Sebaiknya tandatangani hanya URL HTTPS, karena HTTPS menyediakan transportasi aman yang mencegah komponen Signature dari URL bertanda tangan dicegat. Demikian pula, pastikan Anda mendistribusikan URL bertanda tangan melalui protokol transportasi yang aman seperti TLS/HTTPS.