Usar cookies assinados

Nesta página, você terá uma visão geral dos cookies assinados e instruções para usá-los com o Cloud CDN. Os cookies assinados dão acesso a recursos por tempo limitado a um conjunto de arquivos, ainda que os usuários não tenham Contas do Google.

Os cookies assinados são uma alternativa aos URLs assinados. Eles protegem o acesso ao assinar dezenas ou centenas de URLs separadamente para cada usuário no aplicativo.

Os cookies assinados permitem o seguinte:

  • autorizar um usuário e fornecer a ele um token por tempo limitado para acessar seu conteúdo protegido (em vez de assinar cada URL);
  • analisar o acesso do usuário para um prefixo de URL específico, como https://media.example.com/videos/, e conceder ao usuário autorizado acesso ao conteúdo protegido somente nesse prefixo de URL;
  • manter seus URLs e manifestos de mídia inalterados, simplificando o pipeline de empacotamento e melhorando o armazenamento em cache.

Se você quiser definir o acesso a URLs específicos em vez disso, considere usar URLs assinados.

Antes de começar

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

  • Verifique se o Cloud CDN está ativado. Para instruções, consulte Como usar o Cloud CDN. É possível configurar cookies assinados em um back-end antes de ativar o Cloud CDN, mas isso não terá efeito até que o Cloud CDN seja ativado.

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

    gcloud components update
    

Para uma visão geral, consulte URLs e cookies assinados.

Como configurar chaves de solicitação assinadas

A criação de chaves para seus URLs ou cookies assinados requer vários passos, descritos nas seções a seguir.

Considerações sobre segurança

O Cloud CDN não valida solicitações nas seguintes circunstâncias:

  • A solicitação não está assinada.
  • O Cloud CDN ou o serviço de back-end da solicitação não tem o Cloud CDN ativado.

As solicitações assinadas sempre precisam ser validadas na origem antes de exibir a resposta. Isso ocorre porque as origens podem ser usadas para exibir uma mistura de conteúdo assinado e não assinado e porque um cliente pode acessar a origem diretamente.

  • O Cloud CDN não bloqueia solicitações sem um parâmetro de consulta Signature ou um cookie HTTP Cloud-CDN-Cookie. Ele rejeita as solicitações com parâmetros de solicitação inválidos (ou com outros tipos de erros).
  • Quando o aplicativo detectar uma assinatura inválida, verifique se ele 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 solicitações assinadas e não assinadas são armazenadas em cache separadamente. Dessa maneira, uma resposta bem-sucedida a uma solicitação assinada válida nunca é usada para atender a uma solicitação não assinada.
  • Se o aplicativo enviar um código de resposta armazenável em cache para uma solicitação inválida, as futuras solicitações válidas poderão ser rejeitadas incorretamente.

No caso dos back-ends do Cloud Storage, certifique-se de remover o acesso público para que o Cloud Storage possa rejeitar solicitações que não tenham uma assinatura válida.

A tabela a seguir resume o comportamento.

A solicitação tem uma assinatura Ocorrência em cache Comportamento
Não Não Encaminhar para a origem do back-end.
Não Sim Exibir a partir do cache
Sim Não Validar a assinatura. Se válida, encaminhe para a origem do back-end.
Sim Sim Validar a assinatura. Se válida, exibir a partir do cache.

Criar chaves de solicitação assinadas

Para ativar o suporte a URLs e cookies assinados do Cloud CDN, crie uma ou mais chaves em um serviço de back-end com Cloud CDN ativado, um bucket de back-end ou ambos.

Para cada serviço ou bucket de back-end, é possível criar e excluir chaves, conforme as necessidades de segurança. Cada back-end pode ter até três chaves configuradas de cada vez. Sugerimos rotacionar periodicamente suas chaves excluindo a mais antiga, adicionando uma nova e usando-a ao assinar URLs ou cookies.

É possível usar o mesmo nome de chave em vários serviços e buckets de back-end, porque cada conjunto de chaves é independente dos outros. Os nomes das chaves podem ter até 63 caracteres. Para nomear as chaves, use os caracteres de A-Z, a-z, 0-9, _ (sublinhado) e - (hífen).

Ao criar chaves, certifique-se de mantê-las seguras, porque qualquer pessoa que tenha uma das chaves pode criar URLs ou cookies assinados aceitos pelo Cloud CDN até que a chave seja excluída. As chaves são armazenadas no computador em que os URLs ou cookies assinados são gerados. O Cloud CDN também armazena as chaves para verificar as assinaturas das solicitações.

Para manter as chaves em segredo, os valores de chave não são incluídos nas respostas a solicitações de API. Se você perder uma chave, será necessário criar uma nova.

Para criar uma chave de solicitação assinada, siga estas etapas.

Console

  1. No console do Google Cloud, acesse a página do Cloud CDN.

    Acessar o Cloud CDN

  2. Clique no nome da origem em que você quer adicionar a chave.
  3. Na página Detalhes da origem, clique no botão Editar.
  4. Na seção Princípios básicos de origem, clique em Próxima para abrir a seção Regras de host e caminho.
  5. Na seção Regras de host e caminho, clique em Próxima para abrir a seção Desempenho do cache.
  6. Na seção Conteúdo restrito, selecione Restringir o acesso usando URLs e cookies assinados.
  7. Clique em Adicionar chave de assinatura.

    1. Especifique um nome exclusivo para a nova chave de assinatura.
    2. Na seção Método de criação de chave, selecione Gerar automaticamente. Se preferir, clique em Deixe-me inserir e especifique um valor de chave de assinatura.

      No caso da opção anterior, copie o valor da chave de assinatura gerada automaticamente para um arquivo particular, que pode ser usado para criar URLs assinados.

    3. Clique em Concluído.

    4. Na seção Idade máxima da entrada do cache, insira um valor e selecione uma unidade de tempo.

  8. Clique em Concluído.

gcloud

A ferramenta de linha de comando gcloud lê as chaves a partir de um arquivo local que você especificou. Para criar o arquivo de chave, gere 128 bits fortemente aleatórios, codifique-os com base64 e substitua o caractere + por - e o caractere / por _. Para mais informações, consulte a RFC 4648. É essencial que a chave seja fortemente aleatória. Em um sistema semelhante ao UNIX, é possível gerar uma chave fortemente aleatória e armazená-la no arquivo de chave 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 bucket de back-end:

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

Configurar permissões do Cloud Storage

Se você usar o Cloud Storage e tiver restringido quem pode ler os objetos, será necessário conceder permissão ao Cloud CDN para ler os objetos adicionando a conta de serviço do Cloud CDN às ACLs do Cloud Storage.

Não é necessário criar a conta de serviço. Ela é criada automaticamente na primeira vez que você adiciona uma chave a um bucket de back-end em um projeto.

Antes de executar o comando a seguir, adicione pelo menos uma chave a um bucket de back-end no projeto. Caso contrário, o comando falhará com um erro já que a conta de serviço de preenchimento de cache do Cloud CDN não será criada até que você adicione uma ou mais chaves para o projeto.

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

Substitua PROJECT_NUM pelo número do projeto e BUCKET pelo bucket de armazenamento.

A conta de serviço service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com do Cloud CDN não aparece na lista de contas de serviço no seu projeto. Isso acontece porque a conta de serviço do Cloud CDN é propriedade do Cloud CDN, não do seu projeto.

Para mais informações sobre números de projeto, consulte Localizar o ID e o número do projeto na documentação de Ajuda do console do Google Cloud.

Personalizar o tempo máximo de cache

O Cloud CDN armazena em cache as respostas para solicitações assinadas, independentemente do cabeçalho Cache-Control do back-end. O tempo máximo em que as respostas podem ser armazenadas em cache sem revalidação é definido pela sinalização signed-url-cache-max-age, que tem como padrão uma hora e pode ser modificada conforme mostrado aqui.

Para definir o tempo máximo de cache para um serviço ou um bucket de back-end, execute um dos comandos a seguir:

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

Listar os nomes das chaves de solicitação assinadas

Para listar as chaves em um serviço ou bucket de back-end, execute um dos comandos a seguir:

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

Excluir chaves de solicitação assinadas

Quando os URLs assinados por uma chave específica não forem mais aceitos, execute um dos seguintes comandos para excluir essa chave do serviço ou do bucket 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

Como criar uma política

As políticas de cookies assinados são uma série de pares key-value, delimitados pelo caractere :, parecida com os parâmetros de consulta usados em um URL assinado. Para exemplos, consulte Como emitir cookies para usuários.

As políticas representam os parâmetros para os quais uma solicitação é válida. Elas são assinadas usando um código de autenticação de mensagem baseado em hash (HMAC, na sigla em inglês), que o Cloud CDN valida em cada solicitação.

Como definir campos e formato da política

Há quatro campos obrigatórios que você precisa definir na seguinte ordem:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

Os pares key-value em uma política de cookies assinados diferenciam maiúsculas de minúsculas.

URLPrefix

URLPrefix indica um prefixo de URL codificado em Base64 e seguro que abrange todos os caminhos válidos para a assinatura.

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

Por exemplo, https://media.example.com/videos corresponde a solicitações para os seguintes itens:

  • 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, não estritamente como um caminho de diretório. Por exemplo, o prefixo https://example.com/data concede acesso aos dois itens a seguir:

  • /data/file1
  • /database

Para evitar esse erro, recomendamos encerrar todos os prefixos com /, a menos que você opte intencionalmente por encerrar o prefixo com um nome de arquivo 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 solicitado não corresponder a URLPrefix, o Cloud CDN rejeitará a solicitação e retornará um erro HTTP 403 ao cliente.

Expires

Expires precisa ser um carimbo de data/hora Unix (o número de segundos desde 1º de janeiro de 1970).

KeyName

KeyName é o nome da chave criada para o bucket de back-end ou serviço de back-end. Os nomes das chaves diferenciam maiúsculas de minúsculas.

Signature

Signature é a assinatura HMAC-SHA-1 segura para URL e codificada em Base64 dos campos que compõem a política de cookies. Isso é validado em cada solicitação. As solicitações com uma assinatura inválida são rejeitadas com um erro HTTP 403.

Como criar cookies assinados de maneira programática

Os exemplos de código a seguir demonstram como criar cookies assinados de maneira programática.

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

// signCookie creates a signed cookie for an endpoint served by Cloud CDN.
//
// - urlPrefix must start with "https://" and should include the path prefix
// for which the cookie will authorize access to.
// - 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 signCookie(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	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 SignedCookies {

  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 Unix timestamp that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // 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);

    // Create signed cookie from policy.
    String signedCookie = signCookie(urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signedCookie);
  }

  // Creates a signed cookie for the specified policy.
  public static String signCookie(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 policyToSign = String.format("URLPrefix=%s:Expires=%d:KeyName=%s", encodedUrlPrefix,
        expirationTime, keyName);

    String signature = getSignatureForUrl(key, policyToSign);
    return String.format("Cloud-CDN-Cookie=%s:Signature=%s", policyToSign, signature);
  }

  // Creates signature for input string 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
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


def sign_cookie(
    url_prefix: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed cookie value for the specified URL prefix and configuration.

    Args:
        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 Cloud-CDN-Cookie value based on the specified configuration.
    """
    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")

    signed_policy = f"Cloud-CDN-Cookie={policy}:Signature={signature}"

    return signed_policy

Como validar cookies assinados

O processo de validar um cookie assinado é basicamente o mesmo que gerar um cookie assinado. Por exemplo, suponha que você queira validar o seguinte cabeçalho de cookie assinado:

Cookie: Cloud-CDN-Cookie=URLPrefix=URL_PREFIX:Expires=EXPIRATION:KeyName=KEY_NAME:Signature=SIGNATURE; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly

Use a chave secreta denominada KEY_NAME para gerar a assinatura de maneira independente e validar que ela corresponda a SIGNATURE.

Como emitir cookies para os usuários

Seu aplicativo precisa gerar e emitir para cada usuário (cliente) apenas um cookie HTTP contendo uma política assinada corretamente.

  1. Crie um signatário HMAC-SHA-1 no código do aplicativo.

  2. Assine a política usando a chave escolhida, anotando o nome da chave que você adicionou ao back-end, como mySigningKey.

  3. Crie uma política de cookies com o seguinte formato, observando que o nome e o valor diferenciam maiúsculas de minúsculas:

    Name: Cloud-CDN-Cookie
    Value: URLPrefix=$BASE64URLECNODEDURLORPREFIX:Expires=$TIMESTAMP:KeyName=$KEYNAME:Signature=$BASE64URLENCODEDHMAC
    

    Exemplo de cabeçalho Set-Cookie:

    Set-Cookie: Cloud-CDN-Cookie=URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv:Expires=1566268009:KeyName=mySigningKey:Signature=0W2xlMlQykL2TG59UZnnHzkxoaw=; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly
    

    Os atributos Domain e Path no cookie determinam se o cliente envia o cookie para o Cloud CDN.

Recomendações e requisitos

  • Defina explicitamente os atributos Domain e Path para corresponder ao domínio e ao prefixo do caminho de onde o conteúdo protegido será exibido. Ele pode ser diferente do domínio e do caminho em que o cookie é emitido (example.com versus media.example.com ou /browse versus /videos).

  • Verifique se você tem apenas um cookie com um determinado nome para os mesmos Domain e Path.

  • Verifique se você não está emitindo cookies conflitantes, porque isso pode impedir o acesso ao conteúdo em outras sessões do navegador (janelas ou guias).

  • Defina as sinalizações Secure e HttpOnly quando aplicável. Secure garante que o cookie seja enviado somente por conexões HTTPS. HttpOnly impede que o cookie seja disponibilizado para o JavaScript.

  • Os atributos de cookie Expires e Max-Age são opcionais. Se você os omitir, o cookie existirá enquanto a sessão do navegador (guia, janela) existir.

  • Em um preenchimento ou ausência no cache, o cookie assinado é transmitido para a origem definida no serviço de back-end. Verifique se você está validando o valor do cookie assinado em cada solicitação antes de exibir o conteúdo.