Utiliser des URL signées

Cette page présente les URL signées et décrit comment les utiliser avec Cloud CDN. Les URL signées fournissent un accès aux ressources limité dans le temps à toute personne en possession de l'URL, qu'elle ait ou non un compte Google.

Une URL signée est une URL qui fournit une autorisation et une durée limitées pour effectuer une requête. Les URL signées contiennent des informations d'authentification dans leur chaîne de requête, ce qui permet aux utilisateurs sans identifiants d'effectuer des actions spécifiques sur une ressource. Lorsque vous générez une URL signée, vous spécifiez un utilisateur ou un compte de service qui doit disposer d'autorisations suffisantes pour effectuer la requête associée à l'URL.

Une fois l'URL signée générée, toute personne qui en dispose peut l'utiliser pour effectuer des actions spécifiées, telles que la lecture d'un objet, dans un délai spécifié.

Les URL signées acceptent également un paramètre URLPrefix facultatif, qui vous permet d'accorder un accès à plusieurs URL en fonction d'un préfixe commun.

Si vous souhaitez limiter l'accès à un préfixe d'URL spécifique, envisagez d'utiliser des cookies signés.

Avant de commencer

Avant d'utiliser des URL signées, procédez comme suit :

  • Assurez-vous que Cloud CDN est activé. Pour obtenir des instructions, consultez la page Utiliser Cloud CDN. Vous pouvez configurer des URL signées sur un backend avant d'activer Cloud CDN, mais elles sont sans effet jusqu'à l'activation de Cloud CDN.

  • Si nécessaire, installez la dernière version de Google Cloud CLI :

    gcloud components update
    

Pour en savoir plus, consultez la page Cookies et URL signés.

Configurer des clés de requête signées

La création de clés pour vos URL ou cookies signés nécessite plusieurs étapes, décrites dans les sections suivantes.

Points à noter concernant la sécurité

Cloud CDN ne valide pas les requêtes dans les cas suivants :

  • La requête n'est pas signée.
  • Cloud CDN n'est pas activé sur le service de backend ou le bucket backend de la requête.

Les requêtes signées doivent toujours être validées à l'origine avant de diffuser la réponse. Ceci est dû au fait que les origines peuvent servir à diffuser un mélange de contenu signé et non signé, et qu'un client peut accéder directement à l'origine.

  • Cloud CDN ne bloque pas les requêtes n'utilisant ni le paramètre de requête Signature, ni le cookie HTTP Cloud-CDN-Cookie. Cloud CDN rejette les requêtes dont les paramètres sont incorrects (ou ne sont pas rédigés correctement).
  • Lorsque votre application détecte une signature non valide, assurez-vous qu'elle répond avec un code de réponse HTTP 403 (Unauthorized). Les codes de réponse HTTP 403 ne peuvent pas être mis en cache.
  • Les réponses aux requêtes signées et non signées sont mises en cache séparément. Par conséquent, une réponse réussie à une requête signée valide n'est jamais utilisée pour diffuser une requête non signée.
  • Si votre application envoie un code de réponse pouvant être mis en cache à une requête non valide, les futures requêtes valides risquent d'être rejetées par erreur.

Pour les backends Cloud Storage, assurez-vous de supprimer l'accès public afin que Cloud Storage puisse rejeter les requêtes ne contenant pas de signature valide.

Le tableau suivant récapitule le comportement.

La requête comporte une signature Succès de cache (hit) Comportement
Non Non La requête est transférée vers l'origine du backend.
Non Oui La requête est diffusée à partir du cache.
Oui Non La signature est validée. Si la requête est valide, elle est transférée vers l'origine du backend.
Oui Oui La signature est validée. Si la requête est valide, elle est diffusée à partir du cache.

Créer des clés de requête signées

Pour pouvoir exploiter les URL et cookies signés Cloud CDN, vous devez créer une ou plusieurs clés sur un service de backend et/ou un bucket backend compatibles avec Cloud CDN.

Pour chaque service de backend ou bucket backend, vous pouvez créer et supprimer des clés selon vos besoins en termes de sécurité. Chaque backend peut comporter jusqu'à trois clés configurées à la fois. Nous vous suggérons d'effectuer régulièrement une rotation des clés : supprimez la clé la plus ancienne, ajoutez-en une nouvelle et utilisez-la pour la signature des URL ou cookies.

Vous pouvez utiliser le même nom de clé dans plusieurs services de backend et buckets backend, car chaque ensemble de clés est indépendant des autres. Les noms de clé peuvent comporter jusqu'à 63 caractères. Pour nommer vos clés, utilisez les caractères A-Z, a-z, 0-9, _ (trait de soulignement) et - (trait d'union).

Lorsque vous créez des clés, veillez à les sécuriser, car toute personne disposant de l'une de vos clés peut créer des URL ou des cookies signés acceptés par Cloud CDN jusqu'à ce que la clé soit supprimée de Cloud CDN. Les clés sont stockées sur l'ordinateur sur lequel vous générez les URL ou cookies signés. Cloud CDN stocke également les clés pour vérifier les signatures de requêtes.

Pour garder les clés secrètes, les valeurs de clé ne sont pas incluses dans les réponses aux requêtes API. Si vous perdez une clé, vous devez en créer une nouvelle.

Pour créer une clé de requête signée, procédez comme suit :

Console

  1. Dans la console Google Cloud, accédez à la page Cloud CDN.

    Accéder à Cloud CDN

  2. Cliquez sur le nom de l'origine à laquelle vous souhaitez ajouter la clé.
  3. Sur la page Origin details (Détails de l'origine), cliquez sur le bouton Edit (Modifier).
  4. Dans la section Éléments de base de l'origine, cliquez sur Suivant pour ouvrir la section Règles d'hôte et de chemin d'accès.
  5. Dans la section Règles d'hôte et de chemin d'accès, cliquez sur Suivant pour ouvrir la section Performances du cache.
  6. Dans la section Contenu limité, sélectionnez Restreindre l'accès à l'aide d'URL et de cookies signés.
  7. Cliquez sur Ajouter une clé de signature.

    1. Indiquez un nom unique pour la nouvelle clé de signature.
    2. Dans la section Méthode de création de la clé, sélectionnez Générer automatiquement. Vous pouvez également cliquer sur Me laisser entrer et spécifier une valeur de clé de signature.

      Pour la première option, copiez la valeur de la clé de signature générée automatiquement dans un fichier privé, que vous pouvez utiliser pour créer des URL signées.

    3. Cliquez sur OK.

    4. Dans la section Âge maximal de l'entrée de cache, saisissez une valeur, puis sélectionnez une unité de temps.

  8. Cliquez sur OK.

gcloud

L'outil de ligne de commande gcloud lit les clés à partir d'un fichier local que vous spécifiez. Le fichier de clé doit être créé en générant une valeur de 128 bits fortement aléatoire, en l'encodant en base64, puis en remplaçant le caractère + par - et le caractère / par _. Pour plus d'informations, consultez la norme RFC 4648. Il est essentiel que la clé soit fortement aléatoire. Sur un système de type UNIX, vous pouvez générer une clé fortement aléatoire et la stocker dans le fichier de clés à l'aide de la commande suivante :

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

Pour ajouter la clé à un service de backend, utilisez la commande suivante :

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

Pour ajouter la clé à un bucket backend, utilisez la commande ci-dessous :

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

Configurer les autorisations Cloud Storage

Si vous utilisez Cloud Storage et que vous avez restreint l'accès en lecture aux objets, vous devez autoriser Cloud CDN à lire les objets en ajoutant le compte de service Cloud CDN aux listes de contrôle d'accès Cloud Storage.

Vous n'avez pas besoin de créer le compte de service. Il est créé automatiquement la première fois que vous ajoutez une clé à un bucket backend d'un projet.

Avant d'exécuter la commande suivante, ajoutez au moins une clé à un bucket backend de votre projet. Si vous ne respectez pas cette condition, la commande échoue et renvoie une erreur, car le compte de service du remplissage du cache Cloud CDN n'est pas créé tant que vous n'avez pas ajouté au moins une clé pour le projet. Remplacez PROJECT_NUM par le numéro de votre projet et BUCKET par votre bucket de stockage.

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

Le compte de service Cloud CDN, service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com, ne figure pas dans la liste des comptes de service de votre projet. Ceci est dû au fait qu'il appartient à Cloud CDN, et non à votre projet.

Pour plus d'informations sur les numéros de projet, consultez la section Localiser l'ID et le numéro de projet dans la documentation d'aide de Google Cloud Console.

Personnaliser le temps de cache maximal

Cloud CDN met en cache les réponses aux requêtes signées, quel que soit l'en-tête Cache-Control du backend. Le délai maximal de mise en cache des réponses sans revalidation est défini par l'option signed-url-cache-max-age, qui prend la valeur par défaut d'une heure, et peut être modifié comme indiqué ici.

Pour définir la durée maximale de mise en cache d'un service de backend ou d'un bucket backend, exécutez l'une des commandes suivantes :

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

Lister les noms des clés de requête signées

Pour répertorier les clés d'un service de backend ou d'un bucket backend, exécutez l'une des commandes suivantes :

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

Supprimer des clés de requête signées

Lorsque les URL signées par une clé spécifique ne doivent plus être honorées, exécutez l'une des commandes suivantes pour supprimer cette clé du service de backend ou du 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

Signer les URL

La dernière étape consiste à signer les URL et à les distribuer. Vous pouvez signer des URL à l'aide de la commande gcloud compute sign-url ou d'un code que vous écrivez vous-même. Si vous avez besoin d'un grand nombre d'URL signées, le code personnalisé offre de meilleures performances.

Créer des URL signées

Suivez ces instructions pour créer des URL signées à l'aide de la commande gcloud compute sign-url. Pour cette étape, nous partons du principe que vous avez déjà créé les clés.

Console

Vous ne pouvez pas créer d'URL signées à l'aide de la console Google Cloud. Vous pouvez utiliser Google Cloud CLI ou écrire du code personnalisé à partir des exemples suivants.

gcloud

Google Cloud CLI inclut une commande pour la signature d'URL. La commande met en œuvre l'algorithme décrit dans la section sur l'écriture de votre code.

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

Cette commande lit et décode la valeur de clé encodée en base64url de KEY_FILE_NAME, puis génère une URL signée utilisable pour les requêtes GET ou HEAD pour l'URL donnée.

Exemple :

gcloud compute sign-url \
  "https://example.com/media/video.mp4" \
  --key-name my-test-key \
  --expires-in 30m \
  --key-file sign-url-key-file

L'URL URL doit être valide et posséder un composant de chemin. Par exemple, https://example.com/ et https://example.com/whatever sont deux URL valides, contrairement à http://example.com.

Si l'option --validate facultative est spécifiée, cette commande envoie une requête HEAD avec l'URL générée et affiche le code de réponse HTTP. Si l'URL signée est correcte, le code de réponse est le même que le code de résultat envoyé par le backend. Si le code de réponse diffère, revérifiez KEY_NAME et le contenu du fichier spécifié, puis assurez-vous que la valeur de TIME_UNTIL_EXPIRATION est d'au moins plusieurs secondes.

Si l'option --validate n'est pas précisée, les éléments suivants ne sont pas vérifiés :

  • Les entrées
  • L'URL générée
  • L'URL signée générée

Créer des URL signées par programmation

Les exemples de code suivants montrent comment créer des URL signées par programmation.

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

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

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.

    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.utcfromtimestamp(0)
    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}";
}

Créer des URL signées avec un préfixe d'URL par programmation

Les exemples de code suivants montrent comment créer de manière automatisée des URL signées avec un préfixe d'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

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.

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

    policy = f"URLPrefix={encoded_url_prefix}&Expires={expiration_timestamp}&KeyName={key_name}"

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

    return f"{stripped_url}{'&' if query_params else '?'}{policy}&Signature={signature}"

Générer des URL signées personnalisées

Lorsque vous écrivez votre propre code pour générer des URL signées, votre objectif est de créer des URL présentant le format ou l'algorithme suivant. Tous les paramètres d'URL sont sensibles à la casse et doivent être dans l'ordre indiqué :

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

Pour générer des URL signées, procédez comme suit :

  1. Assurez-vous que l'URL de signature ne comporte pas le paramètre de requête Signature.

  2. Déterminez la date et l'heure d'expiration de l'URL, et ajoutez le paramètre de requête Expires avec le délai d'expiration souhaité au format de fuseau horaire UTC (nombre de secondes écoulées depuis le 01/01/1970 à 00:00:00 UTC). Pour plus de sécurité, définissez la valeur sur la période la plus courte possible pour votre cas d'utilisation. Plus la période de validité de l'URL signée est longue, plus le risque de partage accidentel ou intentionnel est élevé.

  3. Définissez le nom de la clé. L'URL doit être signée avec une clé du service de backend ou du bucket backend qui diffuse cette URL. Il est préférable d'utiliser la clé la plus récente pour le renouvellement de la clé. Ajoutez la clé à l'URL en incluant &KeyName=KEY_NAME. Remplacez KEY_NAME par le nom de la clé choisie créée à la section Créer des clés de requête signées.

  4. Signez l'URL. Créez l'URL signée en procédant comme suit. Assurez-vous que les paramètres de requête sont dans l'ordre indiqué immédiatement avant l'étape 1 et qu'aucun élément de l'URL signée ne change de casse.

    a. Hachez l'URL entière (y compris http:// ou https:// au début et &KeyName... à la fin) à l'aide de HMAC-SHA1, avec la clé secrète correspondant au nom de clé choisi auparavant. Utilisez la clé secrète brute de 16 octets, et non la clé encodée en base64url. Décodez-la si nécessaire.

    b. Utilisez l'encodage base64url pour encoder le résultat.

    c. Ajoutez &Signature= à l'URL, suivi de la signature encodée.

Utiliser des préfixes d'URL pour les URL signées

Au lieu de signer l'URL de requête complète avec les paramètres de requête Expires et KeyName, vous pouvez ne signer que les paramètres de requête URLPrefix, Expires et KeyName. Cela vous permet de réutiliser une combinaison donnée de paramètres de requête URLPrefix, Expires, KeyName et Signature tels quels sur plusieurs URL qui correspondent à URLPrefix, vous évitant ainsi d'avoir à créer une signature pour chaque URL.

Dans l'exemple suivant, le texte en surbrillance indique les paramètres que vous signez. Le paramètre Signature est ajouté comme paramètre de requête final, comme d'habitude.

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

Contrairement à la signature d'une URL de requête complète, lorsque vous signez avec URLPrefix, vous ne signez aucun paramètre de requête. Les paramètres de requête peuvent donc être inclus gratuitement dans l'URL. Contrairement aux signatures d'URL de requête complète, ces paramètres de requête supplémentaires peuvent apparaître avant et après les paramètres de requête qui constituent la signature. Par conséquent, l'URL suivante est également valide : elle comporte un préfixe d'URL signée :

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

URLPrefix désigne un préfixe d'URL encodé en base64 et sécurisé pour les URL qui englobe tous les chemins pour lesquels la signature doit être valide.

Un préfixe URLPrefix encode un schéma (http:// ou https://), un nom de domaine complet et un chemin d'accès facultatif. Vous n'avez pas besoin de terminer l'URL par le signe /, mais cette pratique est recommandée. Le préfixe ne doit pas inclure de paramètres de requête ni de fragments tels que ? ou #.

Par exemple, https://media.example.com/videos correspond aux requêtes vers les deux éléments suivants :

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

Le chemin du préfixe est utilisé en tant que sous-chaîne de texte, et non en tant que chemin de répertoire strict. Par exemple, le préfixe https://example.com/data accorde l'accès à ces deux éléments :

  • /data/file1
  • /database

Pour éviter cette erreur, nous vous recommandons de terminer tous les préfixes par /, sauf si vous choisissez de terminer intentionnellement un préfixe par un nom de fichier partiel tel que https://media.example.com/videos/123 pour accorder l'accès aux éléments suivants :

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

Si l'URL demandée ne correspond pas à URLPrefix, Cloud CDN rejette la requête et renvoie une erreur HTTP 403 au client.

Valider les URL signées

Le processus de validation d'une URL signée est, pour l'essentiel, identique à la génération d'une URL signée. Par exemple, supposons que vous souhaitiez valider l'URL signée suivante :

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

Vous pouvez utiliser la clé secrète nommée KEY_NAME pour générer indépendamment la signature de l'URL suivante :

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

Vous pouvez ensuite vérifier qu'elle correspond à SIGNATURE.

Supposons que vous souhaitiez valider une URL signée avec un élément URLPrefix, comme indiqué ci-dessous :

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

Tout d'abord, vérifiez que la valeur décodée en base64 de URL_PREFIX est un préfixe de https://example.com/PATH. Si tel est le cas, vous pouvez calculer la signature pour les éléments suivants :

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

Vous pouvez ensuite vérifier qu'elle correspond à SIGNATURE.

Supprimer l'accès public au bucket Cloud Storage

Pour que les URL signées fournissent une bonne protection au contenu, celui-ci ne doit pas disposer d'un accès public, généralement accordé par le serveur d'origine. Lorsque vous utilisez un bucket Cloud Storage, une méthode courante consiste à rendre les objets publics de manière temporaire pour effectuer des tests. Une fois les URL signées activées, vous devez supprimer les accès en lecture de allUsers (et allAuthenticatedUsers, le cas échéant), autrement dit le rôle IAM Lecteur des objets de l'espace de stockage sur le bucket.

Après avoir désactivé l'accès public sur le bucket, les utilisateurs individuels peuvent toujours accéder à Cloud Storage sans les URL signées s'ils disposent d'autorisations d'accès, telles que l'autorisation Propriétaire.

Pour supprimer l'accès en lecture de allUsers sur un bucket Cloud Storage, inversez le processus décrit dans la section Rendre des groupes d'objets lisibles publiquement.

Distribuer et utiliser des URL signées

L'URL renvoyée par Google Cloud CLI ou produite par votre code personnalisé peut être distribuée selon vos besoins. Nous vous recommandons de ne signer que les URL HTTPS, car ce protocole fournit un transport sécurisé qui empêche le composant Signature de l'URL signée d'être intercepté. De même, vous devez distribuer les URL signées via des protocoles de transport sécurisés tels que TLS/HTTPS.