שימוש בכתובות URL חתומות

בדף הזה מובאת סקירה כללית של כתובות URL חתומות והוראות לשימוש בהן עם Cloud CDN. כתובות URL חתומות מאפשרות לתת גישה למשאבים לזמן מוגבל לכל מי שיש לו את כתובת ה-URL, בין שיש לו חשבון Google ובין אם לא.

כתובת URL חתומה היא כתובת URL עם הרשאה וזמן מוגבלים לשליחת בקשה. כתובות URL חתומות מכילות פרטי אימות במחרוזות השאילתה, ומאפשרות למשתמשים ללא הרשאות לבצע פעולות מסוימות במשאב. כשיוצרים כתובת URL חתומה, צריך לציין משתמש או חשבון שירות שיש להם את ההרשאה הדרושה כדי לשלוח את הבקשה שמשויכת לכתובת ה-URL.

אחרי שיוצרים כתובת URL חתומה, כל מי שיש לו את הכתובת יכול להשתמש בכתובת ה-URL החתומה כדי לבצע פעולות מסוימות (כמו קריאת אובייקט) בפרק זמן מוגדר.

כתובות URL חתומות תומכות גם בפרמטר URLPrefix אופציונלי, שמאפשר לספק גישה למספר כתובות URL על סמך קידומת משותפת.

אם רוצים להגביל את הגישה לקידומת ספציפית של כתובת URL, כדאי להשתמש בקובצי Cookie חתומים.

לפני שמתחילים

לפני שמשתמשים בכתובות URL חתומות, צריך לבצע את הפעולות הבאות:

  • מוודאים ש-Cloud CDN מופעל. הוראות מפורטות זמינות במאמר שימוש ב-Cloud CDN. אפשר להגדיר כתובות URL חתומות בשרת עורפי לפני שמפעילים את Cloud CDN, אבל ההגדרה לא משפיעה עד שמפעילים את Cloud CDN.

  • אם צריך, מעדכנים לגרסה האחרונה של Google Cloud CLI:

    gcloud components update
    

סקירה כללית זמינה במאמר כתובות URL חתומות וקובצי Cookie חתומים.

הגדרת מפתחות של בקשות חתומות

כדי ליצור מפתחות לכתובות URL חתומות או לקובצי Cookie חתומים, צריך לבצע כמה שלבים שמתוארים בקטעים הבאים.

שיקולי אבטחה

‫Cloud CDN לא מאמת בקשות בנסיבות הבאות:

  • הבקשה לא חתומה.
  • שירות לקצה העורפי או קטגוריית קצה עורפי של הבקשה לא מוגדרים עם Cloud CDN.

בקשות חתומות חייבות לעבור אימות במקור תמיד לפני הצגת התגובה. הסיבה לכך היא שאפשר להשתמש במקורות כדי להציג תערובת של תוכן חתום ולא חתום, ולקוח יכול לגשת למקור ישירות.

  • ‫Cloud CDN לא חוסם בקשות ללא פרמטר שאילתה Signature או קובץ Cookie של HTTP‏ Cloud-CDN-Cookie. הוא דוחה בקשות עם פרמטרים לא תקינים (או עם פורמט שגוי).
  • כשהאפליקציה מזהה חתימה לא חוקית, צריך לוודא שהיא מגיבה עם קוד תגובה HTTP 403 (Unauthorized). אי אפשר לשמור במטמון את קודי התגובה HTTP 403.
  • התשובות לבקשות חתומות ולבקשות לא חתומות נשמרות במטמון בנפרד, כך שתשובה מוצלחת לבקשה חתומה ותקינה אף פעם לא משמשת להצגת בקשה לא חתומה.
  • אם האפליקציה שולחת קוד תגובה שניתן לשמירה במטמון לבקשה לא חוקית, יכול להיות שבקשות עתידיות חוקיות יידחו בטעות.

במערכות עורפיות של Cloud Storage, חשוב להסיר את הגישה הציבורית כדי ש-Cloud Storage יוכל לדחות בקשות שחסר בהן חתימה תקפה.

בטבלה הבאה מפורט סיכום של ההתנהגות.

לבקשה יש חתימה מציאה במטמון (cache hit) התנהגות
לא לא העברה למקור העורפי.
לא כן הצגה מהמטמון.
כן לא אימות החתימה. אם התוקף מאומת, המערכת מעבירה את הבקשה למקור העורפי.
כן כן אימות החתימה. אם התוכן תקין, הוא יוגש מהמטמון.

יצירת מפתחות של בקשות חתומות

כדי להפעיל תמיכה בכתובות URL חתומות ובעוגיות חתומות של Cloud CDN, צריך ליצור מפתח אחד או יותר בשירות קצה עורפי, בקטגוריית קצה עורפי או בשניהם, שמופעל בהם Cloud CDN.

לכל שירות לקצה העורפי או קטגוריית קצה עורפי, אפשר ליצור ולמחוק מפתחות בהתאם לצרכי האבטחה. אפשר להגדיר עד שלושה מפתחות לכל קצה עורפי בכל פעם. מומלץ להחליף את המפתחות מדי פעם על ידי מחיקת המפתח הכי ישן, הוספת מפתח חדש ושימוש במפתח החדש כשחותמים על כתובות URL או קובצי Cookie.

אפשר להשתמש באותו שם מפתח בכמה שירותי קצה עורפיים ובכמה דליים של קצה עורפי, כי כל קבוצת מפתחות היא עצמאית. שמות של מפתחות יכולים להכיל עד 63 תווים. כדי לתת שם למפתחות, אפשר להשתמש בתווים A-Z,‏ a-z,‏ 0-9,‏ _ (קו תחתון) ו- (מקף).

כשיוצרים מפתחות, חשוב לשמור עליהם מאובטחים כי כל מי שיש לו אחד מהמפתחות יכול ליצור כתובות URL חתומות או קובצי Cookie חתומים ש-Cloud CDN מקבל עד שהמפתח נמחק מ-Cloud CDN. המפתחות מאוחסנים במחשב שבו יוצרים את כתובות ה-URL החתומות או קובצי ה-Cookie החתומים. בנוסף, Cloud CDN מאחסן את המפתחות כדי לאמת חתימות של בקשות.

כדי לשמור על סודיות המפתחות, ערכי המפתחות לא נכללים בתשובות לבקשות API. אם איבדתם מפתח, אתם צריכים ליצור מפתח חדש.

כדי ליצור מפתח בקשה חתום, פועלים לפי השלבים הבאים.

המסוף

  1. נכנסים לדף Cloud CDN במסוף Google Cloud .

    מעבר אל Cloud CDN

  2. לוחצים על שם המקור שרוצים להוסיף לו את המפתח.
  3. בדף פרטי המקור, לוחצים על הכפתור עריכה.
  4. בקטע Origin basics (הגדרות בסיסיות של מקור), לוחצים על Next (הבא) כדי לפתוח את הקטע Host and path rules (כללים לגבי מארח ונתיב).
  5. בקטע Host and path rules (כללים לגבי מארח ונתיב), לוחצים על Next (הבא) כדי לפתוח את הקטע Cache performance (ביצועים של מטמון).
  6. בקטע תוכן מוגבל, בוחרים באפשרות Restrict access using signed URLs and signed cookies.
  7. לוחצים על הוספת מפתח חתימה.

    1. מציינים שם ייחודי למפתח החתימה החדש.
    2. בקטע שיטת יצירת מפתח, בוחרים באפשרות יצירה אוטומטית. אפשר גם ללחוץ על Let me enter (אני רוצה להזין) ואז לציין ערך של מפתח חתימה.

      באפשרות הראשונה, מעתיקים את הערך של מפתח החתימה שנוצר באופן אוטומטי לקובץ פרטי, שבו אפשר להשתמש כדי ליצור כתובות URL חתומות.

    3. לוחצים על סיום.

    4. בקטע Cache entry maximum age (הגיל המקסימלי של רשומה במטמון), מזינים ערך ואז בוחרים יחידת זמן.

  8. לוחצים על סיום.

gcloud

כלי שורת הפקודה gcloud קורא מפתחות מקובץ מקומי שאתם מציינים. צריך ליצור את קובץ המפתח על ידי יצירת 128 ביטים אקראיים חזקים, קידוד שלהם בפורמט Base64, ואז החלפת התו + ב-- והחלפת התו / ב-_. מידע נוסף זמין ב-RFC 4648. חשוב מאוד שהמפתח יהיה אקראי לחלוטין. במערכת כמו UNIX, אפשר ליצור מפתח אקראי חזק ולאחסן אותו בקובץ המפתח באמצעות הפקודה הבאה:

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

כדי להוסיף את המפתח לשירות לקצה העורפי:

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

כדי להוסיף את המפתח לקטגוריית קצה עורפי:

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

הגדרת הרשאות ב-Cloud Storage

אם אתם משתמשים ב-Cloud Storage והגבלתם את האפשרות לקרוא את האובייקטים, אתם צריכים להוסיף את חשבון השירות של Cloud CDN לרשימות ה-ACL של Cloud Storage כדי לתת ל-Cloud CDN הרשאה לקרוא את האובייקטים.

אין צורך ליצור את חשבון השירות. חשבון השירות נוצר באופן אוטומטי בפעם הראשונה שמוסיפים מפתח למאגר מידע של backend בפרויקט.

לפני שמריצים את הפקודה הבאה, צריך להוסיף לפחות מפתח אחד לקטגוריית קצה עורפי בפרויקט. אחרת, הפקודה תיכשל עם שגיאה כי חשבון השירות למילוי המטמון של Cloud CDN לא נוצר עד שמוסיפים מפתח אחד או יותר לפרויקט.

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

מחליפים את PROJECT_NUMBER במספר הפרויקט ואת BUCKET בקטגוריית האחסון.

חשבון השירות של Cloud CDN‏ service-PROJECT_NUMBER@cloud-cdn-fill.iam.gserviceaccount.com לא מופיע ברשימת חשבונות השירות בפרויקט. הסיבה לכך היא שחשבון השירות של Cloud CDN הוא בבעלות Cloud CDN, ולא בבעלות הפרויקט שלכם.

מידע נוסף על מספרי פרויקטים זמין במאמר איתור מזהה הפרויקט ומספר הפרויקט במסמכי העזרה של מסוף Google Cloud .

התאמה אישית של זמן השהייה המקסימלי במטמון

מערכת Cloud CDN שומרת במטמון תגובות לבקשות חתומות, ללא קשר לכותרת Cache-Control של השרת העורפי. הזמן המקסימלי שבו אפשר לשמור תשובות במטמון בלי לבצע אימות מחדש מוגדר באמצעות הדגל signed-url-cache-max-age, שמוגדר כברירת מחדל לשעה אחת. אפשר לשנות את ההגדרה כמו שמוצג כאן.

כדי להגדיר את זמן השהייה המקסימלי במטמון לשירות לקצה העורפי או לקטגוריית קצה עורפי, מריצים אחת מהפקודות הבאות:

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

רשימה של שמות מפתחות של בקשות חתומות

כדי להציג את המפתחות בשירות לקצה העורפי או בקטגוריית קצה עורפי, מריצים אחת מהפקודות הבאות:

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

מחיקת מפתחות של בקשות חתומות

כדי שכתובות URL שנחתמו על ידי מפתח מסוים לא יכובדו יותר, מריצים אחת מהפקודות הבאות כדי למחוק את המפתח משירות הקצה העורפי או מקטגוריית הקצה העורפי:

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

חתימה על כתובות URL

השלב האחרון הוא לחתום על כתובות ה-URL ולהפיץ אותן. אפשר לחתום על כתובות URL באמצעות הפקודה gcloud compute sign-url או באמצעות קוד שכותבים בעצמכם. אם אתם צריכים הרבה כתובות URL חתומות, קוד בהתאמה אישית יספק ביצועים טובים יותר.

יצירת כתובות URL חתומות

כדי ליצור כתובות URL חתומות באמצעות הפקודה gcloud compute sign-url, פועלים לפי ההוראות האלה. בשלב הזה אנחנו יוצאים מנקודת הנחה שכבר יצרתם את המפתחות.

המסוף

אי אפשר ליצור כתובות URL חתומות באמצעות Google Cloud המסוף. אפשר להשתמש ב-Google Cloud CLI או לכתוב קוד בהתאמה אישית באמצעות הדוגמאות הבאות.

gcloud

‫Google Cloud CLI כולל פקודה לחתימה על כתובות URL. הפקודה מטמיעה את האלגוריתם שמתואר בקטע בנושא כתיבת קוד משלכם.

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

הפקודה הזו קוראת ומפענחת את ערך המפתח בקידוד base64url מ-KEY_FILE_NAME, ואז יוצרת כתובת URL חתומה שאפשר להשתמש בה לבקשות GET או HEAD עבור כתובת ה-URL הנתונה.

לדוגמה:

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

הערך URL חייב להיות כתובת URL תקינה עם רכיב נתיב. לדוגמה, כתובת ה-URL http://example.com לא תקינה, אבל כתובות ה-URL https://example.com/ ו-https://example.com/whatever תקינות.

אם מציינים את האפשרות --validate, הפקודה הזו שולחת בקשת HEAD עם כתובת ה-URL שמתקבלת ומדפיפה את קוד תגובת ה-HTTP. אם כתובת ה-URL החתומה נכונה, קוד התגובה זהה לקוד התוצאה שנשלח על ידי ה-Backend. אם קוד התגובה לא זהה, בודקים שוב את KEY_NAME ואת התוכן של הקובץ שצוין, ומוודאים שהערך של TIME_UNTIL_EXPIRATION הוא לפחות כמה שניות.

אם לא מציינים את הדגל --validate, המערכת לא מאמתת את הפריטים הבאים:

  • נתוני הקלט
  • כתובת ה-URL שנוצרה
  • כתובת ה-URL החתומה שנוצרה

יצירה של כתובות URL חתומות באופן פרוגרמטי

בדוגמאות הקוד הבאות אפשר לראות איך ליצור כתובות URL חתומות באופן פרוגרמטי.

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

יצירה של כתובות URL חתומות באופן פרוגרמטי באמצעות תחילית של כתובת URL

בדוגמאות הקוד הבאות אפשר לראות איך ליצור כתובות URL חתומות באופן פרוגרמטי עם קידומת של כתובת 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}"

יצירת כתובות URL חתומות בהתאמה אישית

כשכותבים קוד משלכם כדי ליצור כתובות URL חתומות, המטרה היא ליצור כתובות URL בפורמט או באלגוריתם הבאים. כל הפרמטרים של כתובות ה-URL הם תלויי-רישיות וחייבים להיות בסדר שמוצג:

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

כדי ליצור כתובות URL חתומות:

  1. מוודאים שכתובת ה-URL לחתימה לא כוללת פרמטר של שאילתה Signature.

  2. קובעים מתי תוקף כתובת ה-URL יפוג ומצרפים פרמטר שאילתה Expires עם זמן התפוגה הנדרש בשעון UTC (מספר השניות מאז 1970-01-01 00:00:00 UTC). כדי למקסם את האבטחה, כדאי להגדיר את הערך לתקופת הזמן הקצרה ביותר האפשרית לתרחיש השימוש שלכם. ככל שכתובת URL חתומה תקפה לזמן ארוך יותר, כך גדל הסיכון שהמשתמש שנתתם לו אותה ישתף אותה עם אחרים, בטעות או בדרך אחרת.

  3. מגדירים את שם המפתח. כתובת ה-URL צריכה להיות חתומה באמצעות מפתח של שירות לקצה העורפי או של קטגוריית קצה עורפי שמשרת את כתובת ה-URL. מומלץ להשתמש במפתח שהוספתם לאחרונה לרוטציית מפתחות. מוסיפים את המפתח לכתובת ה-URL על ידי הוספת &KeyName=KEY_NAME. מחליפים את הערך KEY_NAME בשם של המפתח שנבחר שנוצר בשלב יצירת מפתחות לבקשות חתומות.

  4. חותמים על כתובת ה-URL. כדי ליצור את כתובת ה-URL החתומה: חשוב לוודא שפרמטרי השאילתה מופיעים בסדר שמוצג לפני שלב 1, ולוודא שאין שינוי באותיות הרישיות בכתובת ה-URL החתומה.

    א. מבצעים גיבוב של כתובת ה-URL כולה (כולל http:// או https:// בתחילת הכתובת ו-&KeyName... בסוף הכתובת) באמצעות HMAC-SHA1, על ידי שימוש במפתח הסודי שמתאים לשם המפתח שנבחר קודם. משתמשים במפתח הסודי הגולמי בגודל 16 בייט, ולא במפתח המקודד ב-base64url. מפענחים אותו לפי הצורך.

    ב. משתמשים ב-base64url encode כדי לקודד את התוצאה.

    ג. מוסיפים את &Signature= לכתובת ה-URL, ואחריו את החתימה המקודדת. לא להמיר את התווים = בסוף החתימה לפורמט המקודד שלהם עם סימני אחוזים, %3D.

שימוש בתחיליות של כתובות URL בכתובות URL חתומות

במקום לחתום על כתובת ה-URL המלאה של הבקשה עם פרמטרי השאילתה Expires ו-KeyName, אפשר לחתום רק על פרמטרי השאילתה URLPrefix, Expires ו-KeyName. האפשרות הזו מאפשרת לעשות שימוש חוזר בצירוף נתון של פרמטרים של שאילתה URLPrefix, Expires, KeyName ו-Signature, בדיוק כמו שהוא, בכמה כתובות URL שתואמות ל-URLPrefix, וכך לא צריך ליצור חתימה חדשה לכל כתובת URL שונה.

בדוגמה הבאה, הטקסט המודגש מציג את הפרמטרים שאתם חותמים עליהם. הפרמטר Signature מצורף כפרמטר השאילתה הסופי, כרגיל.

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

בניגוד לחתימה על כתובת URL מלאה של בקשה, כשחותמים באמצעות URLPrefix לא חותמים על פרמטרים של שאילתה, ולכן אפשר לכלול פרמטרים של שאילתה בכתובת ה-URL באופן חופשי. בנוסף, בניגוד לחתימות של כתובות URL מלאות של בקשות, הפרמטרים הנוספים של השאילתה יכולים להופיע לפני ואחרי הפרמטרים של השאילתה שמרכיבים את החתימה. כתוצאה מכך, גם כתובת ה-URL הבאה היא כתובת URL תקינה עם קידומת של כתובת URL חתומה:

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

URLPrefix מציין קידומת של כתובת URL בקידוד Base64 בטוח לשימוש בכתובות URL, שכוללת את כל הנתיבים שהחתימה צריכה להיות תקפה לגביהם.

URLPrefix מקודד סכימה (http:// או https://), שם דומיין מלא (FQDN) ונתיב אופציונלי. הוספת / בסוף הנתיב היא אופציונלית אבל מומלצת. הקידומת לא צריכה לכלול פרמטרים של שאילתה או מקטעים כמו ? או #.

לדוגמה, https://media.example.com/videos מתאים לבקשות לשני המיקומים הבאים:

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

הנתיב של הקידומת משמש כמחרוזת משנה של טקסט, ולא כנתיב של ספרייה. לדוגמה, הקידומת https://example.com/data מעניקה גישה לשני המיקומים הבאים:

  • /data/file1
  • /database

כדי להימנע מהטעות הזו, מומלץ לסיים את כל הקידומות ב-/, אלא אם אתם בוחרים בכוונה לסיים את הקידומת בשם קובץ חלקי כמו https://media.example.com/videos/123 כדי להעניק גישה ל:

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

אם כתובת ה-URL המבוקשת לא תואמת ל-URLPrefix, ‏ Cloud CDN דוחה את הבקשה ומחזיר ללקוח את השגיאה HTTP 403.

אימות של כתובות URL חתומות

תהליך האימות של כתובת URL חתומה זהה בעצם לתהליך היצירה של כתובת URL חתומה. לדוגמה, נניח שרוצים לאמת את כתובת ה-URL החתומה הבאה:

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

אפשר להשתמש במפתח הסודי שנקרא KEY_NAME כדי ליצור באופן עצמאי את החתימה של כתובת ה-URL הבאה:

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

אחר כך תוכלו לוודא שהיא תואמת ל-SIGNATURE.

נניח שרוצים לאמת כתובת URL חתומה עם URLPrefix, כמו בדוגמה הבאה:

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

קודם כול, מוודאים שהערך אחרי פענוח Base64 של URL_PREFIX הוא קידומת של https://example.com/PATH. אם כן, תוכלו לחשב את החתימה עבור:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

אחר כך תוכלו לוודא שהיא תואמת ל-SIGNATURE.

בשיטות חתימה שמבוססות על כתובת URL, שבהן החתימה היא חלק מפרמטרים של שאילתה או מוטמעת כרכיב של נתיב כתובת ה-URL, החתימה והפרמטרים שקשורים אליה מוסרים מכתובת ה-URL לפני שהבקשה נשלחת למקור. כך נמנעות בעיות ניתוב כשהמקור מטפל בבקשה. כדי לאמת את הבקשות האלה, אפשר לבדוק את כותרת הבקשה x-client-request-url, שכוללת את כתובת ה-URL המקורית (החתומות) של בקשת הלקוח לפני ההסרה של הרכיבים החתומים.

הסרת גישה ציבורית לקטגוריה של Cloud Storage

כדי שכתובות URL חתומות יגנו על התוכן בצורה נכונה, חשוב ששרת המקור לא יאפשר גישה ציבורית לתוכן הזה. כשמשתמשים בקטגוריה של Cloud Storage, גישה נפוצה היא להגדיר אובייקטים כציבוריים באופן זמני למטרות בדיקה. אחרי שמפעילים כתובות URL חתומות, חשוב להסיר את הרשאות הגישה allUsers (ו-allAuthenticatedUsers, אם רלוונטי) לקריאה (במילים אחרות, את התפקיד Storage Object Viewer של ניהול זהויות והרשאות גישה) בקטגוריה.

אחרי שמשביתים את הגישה הציבורית לקטגוריה, משתמשים פרטיים עדיין יכולים לגשת ל-Cloud Storage בלי כתובות URL חתומות אם יש להם הרשאת גישה, כמו הרשאת OWNER.

כדי להסיר גישת קריאה ציבורית allUsers לקטגוריה של Cloud Storage, צריך לבצע את הפעולה שמתוארת במאמר הגדרת כל האובייקטים בקטגוריה כקריאים באופן ציבורי.

הפצה ושימוש בכתובות URL חתומות

אפשר להפיץ את כתובת ה-URL שמוחזרת מ-Google Cloud CLI או שנוצרת על ידי הקוד המותאם אישית שלכם בהתאם לצרכים. מומלץ לחתום רק על כתובות URL מסוג HTTPS, כי פרוטוקול HTTPS מספק העברה מאובטחת שמונעת יירוט של רכיב Signature בכתובת ה-URL החתומה. באופן דומה, חשוב לוודא שאתם מפיצים את כתובות ה-URL החתומות באמצעות פרוטוקולי העברה מאובטחים כמו TLS/HTTPS.