Signierte URLs verwenden

Diese Seite bietet eine Übersicht über signierte URLs und eine Anleitung für deren Verwendung mit Cloud CDN. Signierte URLs gewähren zeitlich begrenzten Ressourcenzugriff für alle Personen, denen die URL bekannt ist, unabhängig davon, ob diese Nutzer ein Google-Konto haben.

Eine signierte URL beschränkt die Berechtigung und die Zeit zum Senden einer Anfrage. Abfragestrings von signierten URLs enthalten Authentifizierungsinformationen, damit Nutzer ohne Anmeldedaten bestimmte Aktionen für eine Ressource ausführen können. Wenn Sie eine signierte URL generieren, geben Sie ein Nutzer- oder Dienstkonto an, das ausreichende Berechtigungen hat, um die mit der URL verknüpfte Anfrage auszuführen.

Nachdem Sie eine signierte URL generiert haben, kann sie von allen Personen, denen sie bekannt ist, innerhalb eines bestimmten Zeitraums genutzt werden, um beispielsweise ein Objekt zu lesen.

Signierte URLs unterstützen auch den optionalen Parameter URLPrefix, mit dem Sie Zugriff auf mehrere URLs basierend auf einem gemeinsamen Präfix gewähren können.

Wenn Sie den Zugriff auf ein bestimmtes URL-Präfix beschränken möchten, können Sie signierte Cookies verwenden.

Vorbereitung

Führen Sie die folgenden Schritte aus, bevor Sie signierte URLs verwenden:

  • Achten Sie darauf, dass Cloud CDN aktiviert ist. Eine Anleitung hierzu finden Sie unter Cloud CDN verwenden. Sie können signierte URLs auf einem Backend konfigurieren, bevor Sie Cloud CDN aktivieren. Dies wird jedoch erst wirksam, nachdem Cloud CDN aktiviert wurde.

  • Aktualisieren Sie bei Bedarf auf die neueste Version der Google Cloud CLI:

    gcloud components update
    

Einen Überblick finden Sie unter Signierte URLs und signierte Cookies.

Schlüssel für signierte Anfragen konfigurieren

Schlüssel für signierte URLs oder signierte Cookies werden in mehreren Schritten erstellt, die in den folgenden Abschnitten beschrieben werden.

Sicherheitsaspekte

In den folgenden Fällen validiert Cloud CDN Anfragen nicht:

  • Die Anfrage wurde nicht signiert.
  • Bei dem Backend-Dienst oder Backend-Bucket für die Anfrage ist Cloud CDN nicht aktiviert.

Signierte Anfragen müssen immer am Ursprung validiert werden, bevor die Antwort zurückgegeben wird. Dies liegt daran, dass Ursprünge zur Bereitstellung einer Mischung aus signierten und unsignierten Inhalten verwendet werden können und ein Client direkt auf den Ursprung zugreifen kann.

  • Cloud CDN blockiert keine Anfragen ohne einen Abfrageparameter Signature oder HTTP-Cookie Cloud-CDN-Cookie. Anfragen mit ungültigen oder auf sonstige Weise fehlerhaften Anfrageparametern werden abgelehnt.
  • Wenn die Anwendung eine ungültige Signatur erkennt, prüfen Sie, ob Ihre Anwendung mit dem Antwortcode HTTP 403 (Unauthorized) antwortet. HTTP 403-Antwortcodes können nicht zwischengespeichert werden.
  • Antworten auf signierte und nicht signierte Anfragen werden separat gespeichert. Eine erfolgreiche Antwort auf eine gültige signierte Anfrage wird daher nie zum Bereitstellen einer unsignierten Anfrage verwendet.
  • Wenn Ihre Anwendung einen cachefähigen Antwortcode an eine ungültige Anfrage sendet, werden gültige zukünftige Anfragen eventuell fälschlicherweise abgelehnt.

Achten Sie bei Cloud Storage-Back-Ends darauf, den öffentlichen Zugriff zu entfernen, damit Cloud Storage Anfragen ablehnen kann, denen eine gültige Signatur fehlt.

In der folgenden Tabelle ist die Funktionsweise zusammengefasst.

Anfrage hat Signatur Cache-Treffer Verhalten
Nein Nein An Backend-Ursprung weiterleiten
Nein Ja Aus dem Cache bereitstellen
Ja Nein Signatur validieren. Wenn gültig, an den Backend-Ursprung weiterleiten.
Ja Ja Signatur validieren. Falls gültig, aus dem Cache bereitstellen.

Schlüssel für signierte Anfragen erstellen

Wenn von Cloud CDN signierte URLs und signierte Cookies unterstützt werden sollen, erstellen Sie einen oder mehrere Schlüssel in einem Backend-Dienst und/oder Backend-Bucket mit aktiviertem Cloud CDN.

Für jeden Backend-Dienst oder -Bucket können Sie entsprechend Ihren Sicherheitsanforderungen Schlüssel erstellen und löschen. Für jedes Backend können gleichzeitig bis zu drei Schlüssel konfiguriert sein. Wir empfehlen, Schlüssel regelmäßig zu rotieren. Löschen Sie dazu den ältesten, fügen einen neuen hinzu und verwenden den neuen Schlüssel beim Signieren von URLs und Cookies.

Sie können denselben Schlüsselnamen in mehreren Backend-Diensten und -Buckets verwenden, da jeder Schlüsselsatz von den anderen unabhängig ist. Schlüsselnamen können bis zu 63 Zeichen lang sein. Verwenden Sie zum Benennen Ihrer Schlüssel die Zeichen A–Z, a–z, 0–9, _ (Unterstrich) und - (Bindestrich).

Achten Sie beim Erstellen von Schlüsseln darauf, diese sicher aufzubewahren, da jeder, der einen Ihrer Schlüssel hat, signierte URLs oder signierte Cookies erstellen kann, die Cloud CDN akzeptiert, bis der Schlüssel aus Cloud CDN gelöscht wird. Die Schlüssel werden auf dem Computer gespeichert, auf dem Sie die signierten URLs oder Cookies generieren. Cloud CDN speichert auch die Schlüssel, um Anfragesignaturen zu prüfen.

Damit die Schlüssel geheim bleiben, werden die Schlüssel/Wert-Paare nicht in Antworten auf API-Anfragen aufgenommen. Wenn Sie einen Schlüssel verlieren, müssen Sie einen neuen erstellen.

So erstellen Sie einen Schlüssel für eine signierte Anfrage:

Console

  1. Rufen Sie in der Google Cloud Console die Seite Cloud CDN auf.

    Zu Cloud CDN

  2. Klicken Sie auf den Namen des Ursprungs, dem Sie den Schlüssel hinzufügen möchten.
  3. Klicken Sie auf der Seite Details zum Abflugort auf die Schaltfläche Bearbeiten.
  4. Klicken Sie im Abschnitt Ursprungsgrundlagen auf Weiter, um den Abschnitt Host- und Pfadregeln zu öffnen.
  5. Klicken Sie im Bereich Host- und Pfadregeln auf Weiter, um den Bereich Cache-Leistung zu öffnen.
  6. Wählen Sie im Bereich Eingeschränkte Inhalte die Option Zugriff mit signierten URLs und signierten Cookies einschränken aus.
  7. Klicken Sie auf Signaturschlüssel hinzufügen.

    1. Geben Sie einen eindeutigen Namen für den neuen Signaturschlüssel an.
    2. Wählen Sie im Abschnitt Methode zur Schlüsselerstellung die Option Automatisch generieren aus. Alternativ können Sie auf Selbst eingeben klicken und dann einen Schlüssel/Wert für das Signieren angeben.

      Kopieren Sie für die erste Option das automatisch generierte Schlüssel/Wert-Paar in eine private Datei, in der Sie signierte URLs erstellen können.

    3. Klicken Sie auf Fertig.

    4. Geben Sie im Bereich Höchstalter der Cache-Einträge einen Wert ein und wählen Sie eine Zeiteinheit aus.

  8. Klicken Sie auf Fertig.

gcloud

Das gcloud-Befehlszeilentool liest Schlüssel aus einer von Ihnen angegebenen lokalen Datei. Die Schlüsseldatei muss erstellt werden, indem stark zufällige 128 Bit generiert und mit base64 codiert werden. Anschließend muss das Zeichen „+“ durch „-“ und das Zeichen „/“ durch „_“ ersetzt werden. Weitere Informationen finden Sie unter RFC 4648. Es ist besonders wichtig, dass der Schlüssel stark zufällig ist. Auf einem UNIX-ähnlichen System können Sie einen stark zufälligen Schlüssel generieren und mit dem folgenden Befehl in der Schlüsseldatei speichern:

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

So fügen Sie den Schlüssel zu einem Backend-Dienst hinzu:

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

So fügen Sie den Schlüssel zu einem Backend-Bucket hinzu:

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

Cloud Storage-Berechtigungen konfigurieren

Wenn Sie Cloud Storage verwenden und eingeschränkt haben, wer die Objekte lesen kann, müssen Sie Cloud CDN zum Lesen der Objekte berechtigen. Fügen Sie dazu das Cloud CDN-Dienstkonto den ACLs von Cloud Storage hinzu.

Sie müssen das Dienstkonto nicht erstellen. Das Dienstkonto wird automatisch erstellt, wenn Sie einem Backend-Bucket eines Projekts zum ersten Mal einen Schlüssel hinzufügen.

Fügen Sie mindestens einen Schlüssel zu einem Backend-Bucket in Ihrem Projekt hinzu, bevor Sie den folgenden Befehl ausführen. Ansonsten verursacht der Befehl einen Fehler, da das Dienstkonto von Cloud CDN mit Cache-Füllung erst erstellt wird, wenn Sie mindestens einen Schlüssel für das Projekt hinzufügen. Ersetzen Sie PROJECT_NUM durch Ihre Projektnummer und BUCKET durch Ihren Storage-Bucket.

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

Das Cloud CDN-Dienstkonto service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com ist nicht in der Liste der Dienstkonten in Ihrem Projekt enthalten. Dies liegt daran, dass das Cloud CDN-Dienstkonto zu Cloud CDN gehört und nicht zu Ihrem Projekt.

Weitere Informationen zu Projektnummern finden Sie in der Hilfe zur Google Cloud Console unter Projekt-ID und Projektnummer finden.

Maximale Cache-Zeit anpassen

Cloud CDN speichert Antworten für signierte Anfragen unabhängig vom Cache-Control-Header des Back-Ends im Cache. Die maximale Zeit, für die Antworten ohne erneute Validierung im Cache gespeichert werden können, wird durch das Flag signed-url-cache-max-age festgelegt. Sie beträgt standardmäßig eine Stunde und kann wie hier gezeigt geändert werden.

Führen Sie einen der folgenden Befehle aus, um die maximale Cache-Zeit für einen Backend-Dienst oder -Bucket festzulegen:

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

Schlüsselnamen für signierte Anfragen auflisten

Führen Sie einen der folgenden Befehle aus, um die Schlüssel in einem Backend-Dienst oder -Bucket aufzulisten:

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

Schlüssel für signierte Anfragen löschen

Wenn mit einem bestimmten Schlüssel signierte URLs nicht mehr akzeptiert werden sollen, führen Sie einen der folgenden Befehle aus, um diesen Schlüssel aus dem Backend-Dienst oder Backend-Bucket zu löschen:

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

Urls signieren

Im letzten Schritt werden die URLs signiert und verteilt. Sie können URLs mit dem Befehl gcloud compute sign-url oder durch selbst geschriebenen Code signieren. Wenn Sie viele signierte URLs benötigen, bietet benutzerdefinierter Code eine bessere Leistung.

Signierte URLs erstellen

Folgen Sie dieser Anleitung, um signierte URLs mit dem Befehl gcloud compute sign-url zu erstellen. Bei diesem Schritt wird vorausgesetzt, dass Sie die Schlüssel bereits erstellt haben.

Console

Mit der Google Cloud Console können Sie keine signierten URLs erstellen. Sie können die Google Cloud CLI verwenden oder anhand der folgenden Beispiele benutzerdefinierten Code schreiben.

gcloud

Die Google Cloud CLI enthält einen Befehl zum Signieren von URLs. Der Befehl implementiert den im Abschnitt zum Schreiben eigenen Codes beschriebenen Algorithmus.

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

Der Befehl liest und decodiert den base64url-codierten Schlüsselwert aus KEY_FILE_NAME und gibt dann eine signierte URL aus, die Sie für GET- oder HEAD-Anfragen an die betreffende URL verwenden können.

Beispiel:

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

Die URL muss eine gültige URL mit einer Pfadkomponente sein. http://example.com ist zum Beispiel ungültig, https://example.com/ und https://example.com/whatever hingegen sind gültige URLs.

Wenn das optionale Flag --validate angegeben wird, sendet dieser Befehl eine HEAD-Anfrage mit der resultierenden URL und gibt den HTTP-Antwortcode aus. Wenn die signierte URL korrekt ist, stimmt der Antwortcode mit dem Ergebniscode überein, der von Ihrem Backend gesendet wird. Wenn der Antwortcode nicht identisch ist, prüfen Sie noch einmal KEY_NAME sowie den Inhalt der angegebenen Datei und achten Sie darauf, dass der Wert von TIME_UNTIL_EXPIRATION mindestens einige Sekunden beträgt.

Wenn das Flag --validate nicht angegeben ist, wird Folgendes nicht geprüft:

  • Eingaben
  • Die generierte URL
  • Die generierte signierte URL

Signierte URLs programmatisch erstellen

Die folgenden Codebeispiele zeigen, wie Sie signierte URLs programmatisch erstellen können.

Einfach loslegen (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}";
}

Signierte URLs mit einem URL-Präfix programmatisch erstellen

Die folgenden Codebeispiele zeigen, wie Sie signierte URLs mit einem URL-Präfix programmatisch erstellen.

Einfach loslegen (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}"

Benutzerdefinierte signierte URLs generieren

Wenn Sie eigenen Code für signierte URLs schreiben, besteht das Ziel darin, URLs im folgenden Format oder Algorithmus zu erstellen. Bei allen URL-Parametern muss die Groß-/Kleinschreibung beachtet werden und sie müssen in dieser Reihenfolge angegeben sein:

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

So erstellen Sie signierte URLs:

  1. Achten Sie darauf, dass die URL zum Signieren nicht den Abfrageparameter Signature enthält.

  2. Ermitteln Sie, wann die URL abläuft, und fügen Sie den Abfrageparameter Expires mit dem gewünschten Ablaufzeitpunkt in UTC-Zeit (Anzahl der Sekunden seit 1970-01-01 00:00:00 UTC) an. Für eine maximale Sicherheit setzen Sie den Wert auf den kürzesten Zeitraum, der für Ihren Anwendungsfall möglich ist. Je länger eine signierte URL gültig ist, desto größer ist die Gefahr, dass der Nutzer, dem Sie die URL zur Verfügung stellen, diese versehentlich oder absichtlich an andere weitergibt.

  3. Legen Sie den Schlüsselnamen fest. Die URL muss mit einem Schlüssel des Backend-Dienstes oder Backend-Buckets signiert werden, der die URL bereitstellt. Am besten verwenden Sie den zuletzt hinzugefügten Schlüssel für die Schlüsselrotation. Fügen Sie den Schlüssel der URL hinzu, indem Sie &KeyName=KEY_NAME anhängen. Ersetzen Sie KEY_NAME durch den Namen des ausgewählten Schlüssels, den Sie unter Signierte Anfrageschlüssel erstellen erstellt haben.

  4. Signieren Sie die URL. Erstellen Sie die signierte URL mit den folgenden Schritten. Beachten Sie die direkt vor Schritt 1 gezeigte Reihenfolge der Abfrageparameter. Auch die Groß-/Kleinschreibung in der signierten URL darf nicht geändert werden.

    a. Hashen Sie die gesamte URL, einschließlich http:// oder https:// am Anfang und &KeyName... am Ende, mit HMAC-SHA1. Verwenden Sie dazu den geheimen Schlüssel, der dem oben gewählten Schlüsselnamen entspricht. Nutzen Sie außerdem den geheimen 16-Rohbyte-Schlüssel und nicht den base64url-codierten Schlüssel. Decodieren Sie den Schlüssel, wenn nötig.

    b. Codieren Sie das Ergebnis mit base64url.

    c. Hängen Sie an die URL &Signature= an, gefolgt von der codierten Signatur.

URL-Präfixe für signierte URLs verwenden

Anstatt die vollständige Anfrage-URL mit den Abfrageparametern Expires und KeyName zu signieren, können Sie stattdessen nur die Abfrageparameter URLPrefix, Expires und KeyName signieren. Dadurch kann eine bestimmte Kombination aus den Suchparametern URLPrefix, Expires, KeyName und Signature wortwörtlich für mehrere URLs wiederverwendet werden, die mit dem URLPrefix übereinstimmen. So muss nicht für jede einzelne URL eine neue Signatur erstellt werden.

Im folgenden Beispiel zeigt der markierte Text die Parameter an, die Sie signieren. Signature wird wie üblich als abschließender Abfrageparameter angehängt.

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

Im Gegensatz zum Signieren einer vollständigen Anfrage-URL signieren Sie beim Anmelden mit URLPrefix keine Abfrageparameter. Daher können Abfrageparameter frei in der URL enthalten sein. Im Gegensatz zu vollständigen Anfrage-URL-Signaturen können diese zusätzlichen Abfrageparameter sowohl vor als auch nach den Abfrageparametern der Signatur erscheinen. Daher ist die folgende URL auch eine gültige URL mit einem signierten URL-Präfix:

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

URLPrefix gibt ein URL-sicheres, base64-codiertes URL-Präfix an, das alle Pfade umfasst, für die die Signatur gültig sein soll.

Ein URLPrefix codiert ein Schema (entweder http:// oder https://), einen FQDN und einen optionalen Pfad. Den Pfad mit einem / zu beenden, ist optional, wird jedoch empfohlen. Das Präfix darf keine Suchparameter oder Fragmente wie ? oder # enthalten.

Zum Beispiel ordnet https://media.example.com/videos Anfragen an beide folgenden Plattformen zu:

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

Der Pfad des Präfixes wird als Text-Teilstring und nicht als Verzeichnispfad verwendet. Das Präfix https://example.com/data gewährt beispielsweise Zugriff auf die beiden folgenden Speicherorte:

  • /data/file1
  • /database

Wir empfehlen, allen Präfixen am Ende / hinzuzufügen, um diesen Fehler zu vermeiden, außer Sie lassen das Präfix absichtlich auf einen unvollständigen Dateinamen wie https://media.example.com/videos/123 enden, um Zugriff auf Folgendes zu gewähren:

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

Wenn die angeforderte URL nicht mit URLPrefix übereinstimmt, lehnt Cloud CDN die Anfrage ab und gibt den HTTP-Fehler HTTP 403 an den Client zurück.

Signierte URLs validieren

Die Validierung einer signierten URL entspricht im Wesentlichen der Erstellung einer signierten URL. Angenommen, Sie möchten die folgende signierte URL validieren:

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

Sie können den Secret-Schlüssel mit dem Namen KEY_NAME verwenden, um die Signatur für die folgende URL unabhängig zu generieren:

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

Prüfen Sie dann, ob er mit SIGNATURE übereinstimmt.

Angenommen, Sie möchten eine signierte URL mit einem URLPrefix validieren, wie hier gezeigt:

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

Prüfen Sie zuerst, ob der base64-decodierte Wert von URL_PREFIX ein Präfix von https://example.com/PATH ist. Wenn ja, können Sie die Signatur für Folgendes berechnen:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

Sie können überprüfen, ob er mit SIGNATURE übereinstimmt.

Öffentlichen Zugriff auf den Cloud Storage-Bucket entfernen

Damit signierte URLs Inhalte ordnungsgemäß schützen können, darf der Ursprungsserver keinen öffentlichen Zugriff auf die entsprechenden Inhalte gewähren. Bei Cloud Storage-Buckets werden Objekte in der Regel vorübergehend zu Testzwecken veröffentlicht. Nachdem die signierten URLs aktiviert wurden, müssen die LESEBERECHTIGUNGEN allUsers (und ggf. allAuthenticatedUsers) für den Bucket entfernt werden (mit anderen Worten die IAM-Rolle Storage-Objekt-Betrachter).

Wenn Sie den öffentlichen Zugriff auf den Bucket deaktiviert haben, können einzelne Nutzer dennoch auf Cloud Storage ohne signierte URLs zugreifen, sofern sie eine Zugriffsberechtigung haben, z. B. die Berechtigung INHABER:

Wenn Sie den öffentlichen Lesezugriff allUsers auf einen Cloud Storage-Bucket entfernen möchten, machen Sie die unter Alle Objekte in einem Bucket öffentlich lesbar machen beschriebenen Aktionen rückgängig.

Signierte URLs bereitstellen und verwenden

Die von der Google Cloud CLI zurückgegebene oder von Ihrem benutzerdefinierten Code erstellte URL kann Ihren Anforderungen entsprechend verteilt werden. Wir empfehlen, nur HTTPS-URLs zu signieren, da HTTPS eine sichere Übertragung gewährleistet und dadurch verhindert, dass die Signature-Komponente der signierten URL abgefangen wird. Ebenso sollten Sie die signierten URLs über sichere Übertragungsprotokolle wie TLS/HTTPS verteilen.