Use URLs assinados

Esta página oferece uma vista geral dos URLs assinados e instruções para a respetiva utilização com a RFC do Google Cloud. Os URLs assinados concedem acesso aos recursos por tempo limitado a qualquer pessoa que tenha o URL, quer esta tenha ou não uma Conta Google.

Um URL assinado é um URL que fornece permissão e tempo limitados para fazer um pedido. Os URLs assinados contêm informações de autenticação nas respetivas strings de consulta, o que permite aos utilizadores sem credenciais efetuar ações específicas num recurso. Quando gera um URL assinado, especifica um utilizador ou uma conta de serviço que tem de ter autorização suficiente para fazer o pedido associado ao URL.

Depois de gerar um URL assinado, qualquer pessoa que o possua pode usar o URL assinado para realizar ações específicas (como ler um objeto) num período específico.

Os URLs assinados também suportam um parâmetro URLPrefix opcional, o que lhe permite conceder acesso a vários URLs com base num prefixo comum.

Se quiser restringir o acesso a um prefixo de URL específico, considere usar cookies assinados.

Antes de começar

Antes de usar URLs assinados, faça o seguinte:

  • Certifique-se de que o Cloud CDN está ativado. Para obter instruções, consulte o artigo Usar o Cloud CDN. Pode configurar URLs assinados num back-end antes de ativar o Cloud CDN, mas não têm efeito até o Cloud CDN ser ativado.

  • Se necessário, atualize para a versão mais recente da CLI Google Cloud:

    gcloud components update
    

Para uma vista geral, consulte o artigo URLs assinados e cookies assinados.

Configure chaves de pedidos assinados

A criação de chaves para os seus URLs assinados ou cookies assinados requer vários passos, que são descritos nas secções seguintes.

Considerações de segurança

A RFC de multimédia na nuvem não valida os pedidos nas seguintes circunstâncias:

  • O pedido não está assinado.
  • O serviço de back-end ou o contentor de back-end do pedido não tem o Cloud CDN ativado.

Os pedidos assinados têm de ser sempre validados na origem antes de publicar a resposta. Isto deve-se ao facto de as origens poderem ser usadas para publicar uma combinação de conteúdo assinado e não assinado, e porque um cliente pode aceder diretamente à origem.

  • A RFC não bloqueia pedidos sem um parâmetro de consulta Signature ou um cookie HTTP Cloud-CDN-Cookie. Rejeita pedidos com parâmetros de pedido inválidos (ou com um formato incorreto).
  • Quando a sua aplicação deteta uma assinatura inválida, certifique-se de que a aplicação responde com um código de resposta HTTP 403 (Unauthorized). Os códigos de resposta HTTP 403 não são armazenáveis em cache.
  • As respostas a pedidos assinados e não assinados são colocadas em cache separadamente, pelo que uma resposta bem-sucedida a um pedido assinado válido nunca é usada para publicar um pedido não assinado.
  • Se a sua aplicação enviar um código de resposta memorizável em cache para um pedido inválido, os pedidos futuros válidos podem ser rejeitados incorretamente.

Para backends do Cloud Storage, certifique-se de que remove o acesso público para que o Cloud Storage possa rejeitar pedidos que não tenham uma assinatura válida.

A tabela seguinte resume o comportamento.

O pedido tem assinatura Resultado da cache Comportamento
Não Não Encaminhar para a origem do back-end.
Não Sim Publicar a partir da cache.
Sim Não Validar assinatura. Se for válido, encaminha para a origem do back-end.
Sim Sim Validar assinatura. Se for válido, publicar a partir da cache.

Crie chaves de pedidos assinados

Ativa o suporte para URLs assinados e cookies assinados do Cloud CDN através da criação de uma ou mais chaves num serviço de back-end, num contentor de back-end ou em ambos ativados para o Cloud CDN.

Para cada serviço de back-end ou contentor de back-end, pode criar e eliminar chaves conforme as suas necessidades de segurança. Cada back-end pode ter até três chaves configuradas em simultâneo. Sugerimos que rode periodicamente as chaves eliminando a mais antiga, adicionando uma nova chave e usando a nova chave ao assinar URLs ou cookies.

Pode usar o mesmo nome de chave em vários serviços de back-end e contentores de back-end, porque cada conjunto de chaves é independente dos outros. Os nomes das chaves podem ter até 63 carateres. Para atribuir nomes às chaves, use os carateres A-Z, a-z, 0-9, _ (sublinhado) e - (hífen).

Quando criar chaves, certifique-se de que as mantém seguras, uma vez que qualquer pessoa que tenha uma das suas chaves pode criar URLs assinados ou cookies assinados que o Cloud CDN aceita até que a chave seja eliminada do Cloud CDN. As chaves são armazenadas no computador onde gera os URLs assinados ou os cookies assinados. A RFC na nuvem também armazena as chaves para validar as assinaturas de pedidos.

Para manter as chaves secretas, os valores das chaves não são incluídos nas respostas a pedidos da API. Se perder uma chave, tem de criar uma nova.

Para criar uma chave de pedido assinada, siga estes passos.

Consola

  1. Na Google Cloud consola, aceda à página Cloud CDN.

    Aceda ao Cloud CDN

  2. Clique no nome da origem à qual quer adicionar a chave.
  3. Na página Detalhes da origem, clique no botão Editar.
  4. Na secção Noções básicas de origem, clique em Seguinte para abrir a secção Regras de anfitrião e caminho.
  5. Na secção Regras de anfitrião e caminho, clique em Seguinte para abrir a secção Desempenho da cache.
  6. Na secção Conteúdo restrito, selecione Restringir o acesso através de URLs assinados e cookies assinados.
  7. Clique em Adicionar chave de assinatura.

    1. Especifique um nome exclusivo para a nova chave de assinatura.
    2. Na secção Método de criação de chaves, selecione Gerar automaticamente. Em alternativa, clique em Permitir-me introduzir e, de seguida, especifique um valor da chave de assinatura.

      Para a primeira opção, copie o valor da chave de assinatura gerado automaticamente para um ficheiro privado, que pode usar para criar URLs assinados.

    3. Clique em Concluído.

    4. Na secção Idade máxima da entrada na cache, introduza um valor e, de seguida, selecione uma unidade de tempo.

  8. Clique em Concluído.

gcloud

A ferramenta de linha de comandos gcloud lê chaves de um ficheiro local que especifica. O ficheiro de chave tem de ser criado através da geração de 128 bits fortemente aleatórios, da codificação dos mesmos com base64 e, em seguida, da substituição do caráter + por - e do caráter / por _. Para mais informações, consulte o RFC 4648. É fundamental que a chave seja fortemente aleatória. Num sistema semelhante ao UNIX, pode gerar uma chave fortemente aleatória e armazená-la no ficheiro de chaves com o seguinte comando:

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

Para adicionar a chave a um serviço de back-end:

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

Para adicionar a chave a um contentor de back-end:

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

Configure as autorizações do Cloud Storage

Se usar o Cloud Storage e tiver restringido quem pode ler os objetos, tem de conceder autorização ao Cloud CDN para ler os objetos adicionando a conta de serviço do Cloud CDN às ACLs do Cloud Storage.

Não precisa de criar a conta de serviço. A conta de serviço é criada automaticamente na primeira vez que adiciona uma chave a um contentor de back-end num projeto.

Antes de executar o comando seguinte, adicione, pelo menos, uma chave a um bucket do back-end no seu projeto. Caso contrário, o comando falha com um erro porque a conta de serviço de preenchimento da cache da RFC na nuvem não é criada até adicionar uma ou mais chaves para o projeto.

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

Substitua PROJECT_NUMBER pelo número do seu projeto e BUCKET pelo seu contentor de armazenamento.

A conta de serviço do Cloud CDN service-PROJECT_NUMBER@cloud-cdn-fill.iam.gserviceaccount.com não aparece na lista de contas de serviço no seu projeto. Isto deve-se ao facto de a conta de serviço do Cloud CDN ser propriedade do Cloud CDN e não do seu projeto.

Para mais informações sobre os números dos projetos, consulte o artigo Localize o ID do projeto e o número do projeto na Google Cloud documentação de ajuda da consola.

Personalize o tempo máximo da cache

A RFC na nuvem armazena em cache as respostas a pedidos assinados, independentemente do cabeçalho Cache-Control do back-end. O tempo máximo durante o qual as respostas podem ser colocadas em cache sem revalidação é definido pela flag signed-url-cache-max-age, que tem um valor predefinido de uma hora e pode ser modificada, conforme mostrado aqui.

Para definir o tempo máximo da cache para um serviço de back-end ou um contentor de back-end, execute um dos seguintes comandos:

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

Liste os nomes das chaves de pedidos assinados

Para listar as chaves num serviço de back-end ou num contentor de back-end, execute um dos seguintes comandos:

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

Elimine chaves de pedidos assinados

Quando os URLs assinados por uma chave específica já não devem ser respeitados, execute um dos seguintes comandos para eliminar essa chave do serviço de back-end ou do contentor de back-end:

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

Assine URLs

O último passo é assinar os URLs e distribuí-los. Pode assinar URLs através do comando gcloud compute sign-url ou através de código escrito por si. Se precisar de muitos URLs assinados, o código personalizado oferece um melhor desempenho.

Crie URLs assinados

Siga estas instruções para criar URLs assinados com o comando gcloud compute sign-url. Este passo pressupõe que já criou as chaves.

Consola

Não pode criar URLs assinados através da Google Cloud consola. Pode usar a CLI do Google Cloud ou escrever código personalizado usando os exemplos seguintes.

gcloud

A CLI do Google Cloud inclui um comando para assinar URLs. O comando implementa o algoritmo descrito na secção sobre escrever o seu próprio código.

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

Este comando lê e descodifica o valor da chave codificado em base64url de KEY_FILE_NAME e, em seguida, produz um URL assinado que pode usar para pedidos GET ou HEAD para o URL fornecido.

Por exemplo:

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

O URL tem de ser um URL válido com um componente de caminho. Por exemplo, http://example.com é inválido, mas https://example.com/ e https://example.com/whatever são URLs válidos.

Se for fornecida a flag opcional --validate, este comando envia um pedido com o URL resultante e imprime o código de resposta HTTP.HEAD Se o URL assinado estiver correto, o código de resposta é igual ao código de resultado enviado pelo seu back-end. Se o código de resposta não for o mesmo, verifique novamente KEY_NAME e o conteúdo do ficheiro especificado, e certifique-se de que o valor de TIME_UNTIL_EXPIRATION é, pelo menos, de vários segundos.

Se a flag --validate não for apresentada, o seguinte não é validado:

  • As entradas
  • O URL gerado
  • O URL assinado gerado

Crie URLs assinados de forma programática

Os seguintes exemplos de código demonstram como criar URLs assinados programaticamente.

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

Crie URLs assinados de forma programática com um prefixo de URL

Os exemplos de código seguintes demonstram como criar URLs assinados programaticamente com um prefixo de 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}"

Gere URLs assinados personalizados

Quando escreve o seu próprio código para gerar URLs assinados, o objetivo é criar URLs com o seguinte formato ou algoritmo. Todos os parâmetros de URL são sensíveis a maiúsculas e minúsculas e têm de estar na ordem apresentada:

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

Para gerar URLs assinados, siga estes passos:

  1. Certifique-se de que o URL para a assinatura não tem um parâmetro de consulta Signature.

  2. Determine quando o URL expira e acrescente um parâmetro de consulta Expires com o tempo de expiração necessário em tempo UTC (o número de segundos desde 1970-01-01 00:00:00 UTC). Para maximizar a segurança, defina o valor para o período mais curto possível para o seu exemplo de utilização. Quanto mais tempo um URL assinado for válido, maior é o risco de o utilizador a quem o dá o partilhar com outras pessoas, acidentalmente ou não.

  3. Defina o nome da chave. O URL tem de ser assinado com uma chave do serviço de back-end ou do contentor de back-end que serve o URL. É recomendável usar a chave adicionada mais recentemente para a alteração da chave. Adicione a chave ao URL acrescentando &KeyName=KEY_NAME. Substitua KEY_NAME pelo nome da chave escolhida criada em Criar chaves de pedidos assinados.

  4. Assine o URL. Crie o URL assinado seguindo estes passos. Certifique-se de que os parâmetros de consulta estão na ordem apresentada imediatamente antes do passo 1 e de que nada no URL assinado altera as letras maiúsculas/minúsculas.

    a. Faça o hash do URL completo (incluindo http:// ou https:// no início e &KeyName... no final) com HMAC-SHA1 usando a chave secreta que corresponde ao nome da chave escolhido anteriormente. Use a chave secreta de 16 bytes não processada e não a chave codificada em base64url. Descodifique-o, se necessário.

    b. Use a codificação base64url para codificar o resultado.

    c. Anexe &Signature= ao URL, seguido da assinatura codificada. Não converta os carateres = finais da assinatura na respetiva forma codificada em percentagem, %3D.

Use prefixos de URL para URLs assinados

Em vez de assinar o URL de pedido completo com os parâmetros de consulta Expires e KeyName, pode assinar apenas os parâmetros de consulta URLPrefix, Expires e KeyName. Isto permite que uma determinada combinação de parâmetros de consulta URLPrefix, Expires, KeyName e Signature seja reutilizada literalmente em vários URLs que correspondam ao URLPrefix, evitando a necessidade de criar uma nova assinatura para cada URL distinto.

No exemplo seguinte, o texto realçado mostra os parâmetros que assina. O Signature é anexado como o parâmetro de consulta final, como habitualmente.

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

Ao contrário da assinatura de um URL de pedido completo, quando assina com URLPrefix, não assina nenhum parâmetro de consulta, pelo que os parâmetros de consulta podem ser incluídos livremente no URL. Além disso, ao contrário das assinaturas de URL de pedido completo, esses parâmetros de consulta adicionais podem aparecer antes e depois dos parâmetros de consulta que compõem a assinatura. Como resultado, o seguinte também é um URL válido com um prefixo de URL assinado:

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

URLPrefix denota um prefixo de URL codificado em base64 seguro para URL que abrange todos os caminhos para os quais a assinatura deve ser válida.

Um URLPrefix codifica um esquema (http:// ou https://), um FQDN e um caminho opcional. Terminar o caminho com um / é opcional, mas recomendado. O prefixo não deve incluir parâmetros de consulta nem fragmentos, como ? ou #.

Por exemplo, https://media.example.com/videos corresponde a pedidos a ambos os seguintes:

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

O caminho do prefixo é usado como uma substring de texto e não estritamente como um caminho de diretório. Por exemplo, o prefixo https://example.com/data concede acesso a ambos os seguintes elementos:

  • /data/file1
  • /database

Para evitar este erro, recomendamos que termine todos os prefixos com /, a menos que opte intencionalmente por terminar o prefixo com um nome de ficheiro parcial, como https://media.example.com/videos/123, para conceder acesso ao seguinte:

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

Se o URL pedido não corresponder ao URLPrefix, a RFC rejeita o pedido e devolve um erro HTTP 403 ao cliente.

Valide URLs assinados

O processo de validação de um URL assinado é essencialmente o mesmo que o de geração de um URL assinado. Por exemplo, suponha que quer validar o seguinte URL assinado:

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

Pode usar a chave secreta com o nome KEY_NAME para gerar independentemente a assinatura para o seguinte URL:

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

Em seguida, pode verificar se corresponde a SIGNATURE.

Suponhamos que quer validar um URL assinado que tem um URLPrefix, conforme mostrado aqui:

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

Primeiro, verifique se o valor descodificado em base64 de URL_PREFIX é um prefixo de https://example.com/PATH. Se for o caso, pode calcular a assinatura para o seguinte:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

Em seguida, pode verificar se corresponde a SIGNATURE.

Para métodos de assinatura baseados em URL, em que a assinatura faz parte dos parâmetros de consulta ou está incorporada como um componente do caminho do URL, a assinatura e os parâmetros relacionados são removidos do URL antes de o pedido ser enviado para a origem. Isto impede que a assinatura cause problemas de encaminhamento quando a origem está a processar o pedido. Para validar estes pedidos, pode inspecionar o cabeçalho do pedido x-client-request-url, que inclui o URL do pedido do cliente original (assinado) antes da remoção dos componentes assinados.

Remova o acesso público ao contentor do Cloud Storage

Para que os URLs assinados protejam corretamente o conteúdo, é importante que o servidor de origem não conceda acesso público a esse conteúdo. Quando usa um contentor do Cloud Storage, uma abordagem comum é tornar os objetos públicos temporariamente para fins de teste. Depois de ativar os URLs assinados, é importante remover as autorizações de LEITURA allUsers (e allAuthenticatedUsers, se aplicável) (por outras palavras, a função de gestão de identidades e acessos Storage Object Viewer) no contentor.

Depois de desativar o acesso público no contentor, os utilizadores individuais continuam a poder aceder ao Cloud Storage sem URLs assinados se tiverem autorização de acesso, como autorização de PROPRIETÁRIO.

Para remover o acesso de allUsers LEITURA público num contentor do Cloud Storage, inverta a ação descrita no artigo Tornar todos os objetos num contentor publicamente legíveis.

Distribua e use URLs assinados

O URL devolvido pela Google Cloud CLI ou produzido pelo seu código personalizado pode ser distribuído de acordo com as suas necessidades. Recomendamos que assine apenas URLs HTTPS, porque o HTTPS oferece um transporte seguro que impede a interceção do componente Signature do URL assinado. Da mesma forma, certifique-se de que distribui os URLs assinados através de protocolos de transporte seguros, como TLS/HTTPS.