Como 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 fornecem 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 do SDK do 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

É preciso configurar os servidores da Web de origem para validar as assinaturas em todas as solicitações assinadas e aceitar ou rejeitar solicitações não assinadas.

  • O Cloud CDN não bloqueia solicitações sem um parâmetro de consulta Signature. Rejeita 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 retorna um código de resposta HTTP 403 (Unauthorized). Os códigos de resposta de HTTP 403 não são armazenáveis em cache.
  • Se o aplicativo enviar um código de resposta armazenável em cache para uma solicitação inválida, as solicitações futuras válidas poderão ser rejeitadas incorretamente.

Para os back-ends do Cloud Storage, certifique-se de ter removido o acesso público, que permite que o Cloud Storage rejeite as solicitações que não têm uma assinatura válida.

Como 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 AZ, az, 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 chaves, siga estas etapas:

Console

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

    Acessar a página do Cloud CDN

  2. Clique em Adicionar origem.
  3. Selecione um balanceador de carga HTTP(S) como origem.
  4. Selecione serviços ou buckets de back-end. Para cada um deles, faça o seguinte:
    1. Clique em Configurar e, em seguida, em Adicionar chave de assinatura.
    2. Em Nome, atribua um nome para a nova chave de assinatura.
    3. Em Método de criação de chave, selecione Gerar automaticamente ou Quero inserir.
    4. Se você estiver digitando sua própria chave, digite-a no campo de texto.
    5. Clique em Concluído.
    6. Em Idade máxima da entrada do cache, forneça um valor e selecione uma Unidade de tempo na lista suspensa. É possível escolher entre segundo, minuto, hora, e dia. O período máximo de tempo é de três (3) dias.
  5. Clique em Save.
  6. Clique em Adicionar.

gcloud

A ferramenta de linha de comando gcloud lê as chaves 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 46485 (em inglês). É 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

Como 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ê inclua uma ou mais chaves para o projeto. Substitua PROJECT_NUM pelo número do projeto e BUCKET pelo bucket de armazenamento.

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

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

Como personalizar opcionalmente 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

Como listar nomes de chave 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

Como 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 a 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 o cookie.

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 ativamente 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 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
}

// readKeyFile reads the base64url-encoded key file and decodes it.
func readKeyFile(path string) ([]byte, error) {
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read key file: %+v", err)
	}
	d := make([]byte, base64.URLEncoding.DecodedLen(len(b)))
	n, err := base64.URLEncoding.Decode(d, b)
	if err != nil {
		return nil, fmt.Errorf("failed to base64url decode: %+v", err)
	}
	return d[:n], nil
}

func generateSignedCookie(w io.Writer) error {
	// The path to a file containing the base64-encoded signing key
	keyPath := os.Getenv("KEY_PATH")

	// Note: consider using the GCP Secret Manager for managing access to your
	// signing key(s).
	key, err := readKeyFile(keyPath)
	if err != nil {
		return err
	}

	var (
		// domain and path should match the user-facing URL for accessing
		// content.
		domain     = "media.example.com"
		path       = "/segments/"
		keyName    = "my-key"
		expiration = time.Hour * 2
	)

	signedValue, err := signCookie(fmt.Sprintf("https://%s%s", domain,
		path), keyName, key, time.Now().Add(expiration))
	if err != nil {
		return err
	}

	// Use Go's http.Cookie type to construct a cookie.
	cookie := &http.Cookie{
		Name:   "Cloud-CDN-Cookie",
		Value:  signedValue,
		Path:   path, // Best practice: only send the cookie for paths it is valid for
		Domain: domain,
		MaxAge: int(expiration.Seconds()),
	}

	// We print this to stdout in this example. In a real application, use the
	// SetCookie method on a http.ResponseWriter to write the cookie to the
	// user.
	fmt.Fprintln(w, cookie)

	return nil
}

Python

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

    Args:
        url_prefix: URL prefix to sign as a string.
        key_name: name of the signing key as a string.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as a UTC datetime object.

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

    policy_pattern = u'URLPrefix={encoded_url_prefix}:Expires={expires}:KeyName={key_name}'
    policy = policy_pattern.format(
            encoded_url_prefix=encoded_url_prefix,
            expires=expiration_timestamp,
            key_name=key_name)

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

    signed_policy = u'Cloud-CDN-Cookie={policy}:Signature={signature}'.format(
            policy=policy, signature=signature)
    print(signed_policy)

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 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.