토큰 생성

이 가이드에서는 토큰 생성 방법과 토큰의 필수 및 선택적 필드에 대해 설명합니다.

토큰을 만들려면 이 가이드에서 서명 값이라고 부르는 서명할 문자열을 구성합니다. 서명 값에는 보호 대상 콘텐츠, 서명 값의 만료 시간 등을 기술하는 매개변수가 포함됩니다.

토큰 문자열을 만드는 동안 서명 값을 사용합니다. 서명 값의 대칭 키 해시 기반 메시지 인증 코드(HMAC)와 같이 토큰의 매개변수를 구성하여 토큰 문자열을 만듭니다.

Media CDN은 콘텐츠 보호를 돕기 위해 최종 구성 토큰을 사용합니다.

토큰 만들기

  1. 필수 토큰 필드와 원하는 선택적 토큰 필드가 포함된 문자열을 연결하여 서명 값을 만듭니다. 틸드 ~ 문자를 사용해서 각 필드와 매개변수를 분리합니다.

  2. Ed25519 서명 또는 대칭 키 HMAC를 사용해서 서명된 키를 서명합니다.

  3. 필수 토큰 필드와 선택적 토큰 필드가 포함된 문자열을 연결하여 토큰을 구성합니다. 틸드 ~ 문자를 사용해서 각 필드와 매개변수를 분리합니다.

    토큰을 구성할 때 각 매개변수의 값은 서명 값과 토큰 문자열 사이에 동일하지만, 다음과 같은 예외가 있습니다.

    • FullPath
    • Headers

다음 코드 샘플은 토큰을 프로그래매틱 방법으로 만드는 방법을 보여줍니다.

Python

Media CDN에 인증하려면 애플리케이션 기본 사용자 인증 정보를 설정합니다. 자세한 내용은 로컬 개발 환경의 인증 설정을 참조하세요.

import base64
import datetime
import hashlib
import hmac

import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519


def base64_encoder(value: bytes) -> str:
    """
    Returns a base64-encoded string compatible with Media CDN.

    Media CDN uses URL-safe base64 encoding and strips off the padding at the
    end.
    """
    encoded_bytes = base64.urlsafe_b64encode(value)
    encoded_str = encoded_bytes.decode("utf-8")
    return encoded_str.rstrip("=")


def sign_token(
    base64_key: bytes,
    signature_algorithm: str,
    start_time: datetime.datetime = None,
    expiration_time: datetime.datetime = None,
    url_prefix: str = None,
    full_path: str = None,
    path_globs: str = None,
    session_id: str = None,
    data: str = None,
    headers: str = None,
    ip_ranges: str = None,
) -> str:
    """Gets the Signed URL Suffix string for the Media CDN' Short token URL requests.
    One of (`url_prefix`, `full_path`, `path_globs`) must be included in each input.
    Args:
        base64_key: Secret key as a base64 encoded string.
        signature_algorithm: Algorithm can be either `SHA1` or `SHA256` or `Ed25519`.
        start_time: Start time as a UTC datetime object.
        expiration_time: Expiration time as a UTC datetime object. If None, an expiration time 1 hour from now will be used.
        url_prefix: the URL prefix to sign, including protocol.
                    For example: http://example.com/path/ for URLs under /path or http://example.com/path?param=1
        full_path:  A full path to sign, starting with the first '/'.
                    For example: /path/to/content.mp4
        path_globs: a set of ','- or '!'-delimited path glob strings.
                    For example: /tv/*!/film/* to sign paths starting with /tv/ or /film/ in any URL.
        session_id: a unique identifier for the session
        data: data payload to include in the token
        headers: header name and value to include in the signed token in name=value format.  May be specified more than once.
                    For example: [{'name': 'foo', 'value': 'bar'}, {'name': 'baz', 'value': 'qux'}]
        ip_ranges: A list of comma separated ip ranges. Both IPv4 and IPv6 ranges are acceptable.
                    For example: "203.0.113.0/24,2001:db8:4a7f:a732/64"

    Returns:
        The Signed URL appended with the query parameters based on the
        specified URL prefix and configuration.
    """

    decoded_key = base64.urlsafe_b64decode(base64_key)
    algo = signature_algorithm.lower()

    # For most fields, the value we put in the token and the value we must sign
    # are the same.  The FullPath and Headers use a different string for the
    # value to be signed compared to the token.  To illustrate this difference,
    # we'll keep the token and the value to be signed separate.
    tokens = []
    to_sign = []

    # check for `full_path` or `path_globs` or `url_prefix`
    if full_path:
        tokens.append("FullPath")
        to_sign.append(f"FullPath={full_path}")
    elif path_globs:
        path_globs = path_globs.strip()
        field = f"PathGlobs={path_globs}"
        tokens.append(field)
        to_sign.append(field)
    elif url_prefix:
        field = "URLPrefix=" + base64_encoder(url_prefix.encode("utf-8"))
        tokens.append(field)
        to_sign.append(field)
    else:
        raise ValueError(
            "User Input Missing: One of `url_prefix`, `full_path` or `path_globs` must be specified"
        )

    # check & parse optional params
    if start_time:
        epoch_duration = start_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
        field = f"Starts={int(epoch_duration.total_seconds())}"
        tokens.append(field)
        to_sign.append(field)

    if not expiration_time:
        expiration_time = datetime.datetime.now() + datetime.timedelta(hours=1)
        epoch_duration = expiration_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
    else:
        epoch_duration = expiration_time.astimezone(
            tz=datetime.timezone.utc
        ) - datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)
    field = f"Expires={int(epoch_duration.total_seconds())}"
    tokens.append(field)
    to_sign.append(field)

    if session_id:
        field = f"SessionID={session_id}"
        tokens.append(field)
        to_sign.append(field)

    if data:
        field = f"Data={data}"
        tokens.append(field)
        to_sign.append(field)

    if headers:
        header_names = []
        header_pairs = []
        for each in headers:
            header_names.append(each["name"])
            header_pairs.append("%s=%s" % (each["name"], each["value"]))
        tokens.append(f"Headers={','.join(header_names)}")
        to_sign.append(f"Headers={','.join(header_pairs)}")

    if ip_ranges:
        field = f"IPRanges={base64_encoder(ip_ranges.encode('ascii'))}"
        tokens.append(field)
        to_sign.append(field)

    # generating token
    to_sign = "~".join(to_sign)
    to_sign_bytes = to_sign.encode("utf-8")
    if algo == "ed25519":
        digest = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key).sign(
            to_sign_bytes
        )
        tokens.append("Signature=" + base64_encoder(digest))
    elif algo == "sha256":
        signature = hmac.new(
            decoded_key, to_sign_bytes, digestmod=hashlib.sha256
        ).hexdigest()
        tokens.append("hmac=" + signature)
    elif algo == "sha1":
        signature = hmac.new(
            decoded_key, to_sign_bytes, digestmod=hashlib.sha1
        ).hexdigest()
        tokens.append("hmac=" + signature)
    else:
        raise ValueError(
            "Input Missing Error: `signature_algorithm` can only be one of `sha1`, `sha256` or `ed25519`"
        )
    return "~".join(tokens)

Java

Media CDN에 인증하려면 애플리케이션 기본 사용자 인증 정보를 설정합니다. 자세한 내용은 로컬 개발 환경의 인증 설정을 참조하세요.


import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;

public class DualToken {

  public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
    // TODO(developer): Replace these variables before running the sample.
    // Secret key as a base64 encoded string.
    byte[] base64Key = new byte[]{};
    // Algorithm can be one of these: SHA1, SHA256, or Ed25519.
    String signatureAlgorithm = "ed25519";
    // (Optional) Start time as a UTC datetime object.
    DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
    Optional<Instant> startTime = Optional.empty();
    // Expiration time as a UTC datetime object.
    // If None, an expiration time that's an hour after the current time is used.
    Instant expiresTime = Instant.from(formatter.parse("2022-09-13T12:00:00Z"));

    // ONE OF (`urlPrefix`, `fullPath`, `pathGlobs`) must be included in each input.
    // The URL prefix and protocol to sign.
    // For example: http://example.com/path/ for URLs under /path or http://example.com/path?param=1
    Optional<String> urlPrefix = Optional.empty();
    // A full path to sign, starting with the first '/'.
    // For example: /path/to/content.mp4
    Optional<String> fullPath = Optional.of("http://10.20.30.40/");
    // A set of path glob strings delimited by ',' or '!'.
    // For example: /tv/*!/film/* to sign paths starting with /tv/ or /film/ in any URL.
    Optional<String> pathGlobs = Optional.empty();

    // (Optional) A unique identifier for the session.
    Optional<String> sessionId = Optional.empty();
    // (Optional) Data payload to include in the token.
    Optional<String> data = Optional.empty();
    // (Optional) Header name and value to include in the signed token in name=value format.
    // May be specified more than once.
    // For example: [{'name': 'foo', 'value': 'bar'}, {'name': 'baz', 'value': 'qux'}]
    Optional<List<Header>> headers = Optional.empty();
    // (Optional) A list of comma-separated IP ranges. Both IPv4 and IPv6 ranges are acceptable.
    // For example: "203.0.113.0/24,2001:db8:4a7f:a732/64"
    Optional<String> ipRanges = Optional.empty();

    DualToken.signToken(
        base64Key,
        signatureAlgorithm,
        startTime,
        expiresTime,
        urlPrefix,
        fullPath,
        pathGlobs,
        sessionId,
        data,
        headers,
        ipRanges);
  }

  // Gets the signed URL suffix string for the Media CDN short token URL requests.
  // Result:
  //     The signed URL appended with the query parameters based on the
  // specified URL prefix and configuration.
  public static void signToken(
      byte[] base64Key, String signatureAlgorithm, Optional<Instant> startTime,
      Instant expirationTime, Optional<String> urlPrefix, Optional<String> fullPath,
      Optional<String> pathGlobs, Optional<String> sessionId, Optional<String> data,
      Optional<List<Header>> headers, Optional<String> ipRanges)
      throws NoSuchAlgorithmException, InvalidKeyException {

    String field = "";
    byte[] decodedKey = Base64.getUrlDecoder().decode(base64Key);

    // For most fields, the value in the token and the value to sign
    // are the same. Compared to the token, the FullPath and Headers
    // use a different string for the value to sign. To illustrate this difference,
    // we'll keep the token and the value to be signed separate.
    List<String> tokens = new ArrayList<>();
    List<String> toSign = new ArrayList<>();

    // Check for `fullPath` or `pathGlobs` or `urlPrefix`.
    if (fullPath.isPresent()) {
      tokens.add("FullPath");
      toSign.add(String.format("FullPath=%s", fullPath.get()));
    } else if (pathGlobs.isPresent()) {
      field = String.format("PathGlobs=%s", pathGlobs.get().trim());
      tokens.add(field);
      toSign.add(field);
    } else if (urlPrefix.isPresent()) {
      field = String.format("URLPrefix=%s",
          base64Encoder(urlPrefix.get().getBytes(StandardCharsets.UTF_8)));
      tokens.add(field);
      toSign.add(field);
    } else {
      throw new IllegalArgumentException(
          "User Input Missing: One of `urlPrefix`, `fullPath` or `pathGlobs` must be specified");
    }

    // Check & parse optional params.
    long epochDuration;
    if (startTime.isPresent()) {
      epochDuration = ChronoUnit.SECONDS.between(Instant.EPOCH, startTime.get());
      field = String.format("Starts=%s", epochDuration);
      tokens.add(field);
      toSign.add(field);
    }

    if (expirationTime == null) {
      expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
    }
    epochDuration = ChronoUnit.SECONDS.between(Instant.EPOCH, expirationTime);
    field = String.format("Expires=%s", epochDuration);
    tokens.add(field);
    toSign.add(field);

    if (sessionId.isPresent()) {
      field = String.format("SessionID=%s", sessionId.get());
      tokens.add(field);
      toSign.add(field);
    }

    if (data.isPresent()) {
      field = String.format("Data=%s", data.get());
      tokens.add(field);
      toSign.add(field);
    }

    if (headers.isPresent()) {
      List<String> headerNames = new ArrayList<>();
      List<String> headerPairs = new ArrayList<>();

      for (Header entry : headers.get()) {
        headerNames.add(entry.getName());
        headerPairs.add(String.format("%s=%s", entry.getName(), entry.getValue()));
      }
      tokens.add(String.format("Headers=%s", String.join(",", headerNames)));
      toSign.add(String.format("Headers=%s", String.join(",", headerPairs)));
    }

    if (ipRanges.isPresent()) {
      field = String.format("IPRanges=%s",
          base64Encoder(ipRanges.get().getBytes(StandardCharsets.US_ASCII)));
      tokens.add(field);
      toSign.add(field);
    }

    // Generate token.
    String toSignJoined = String.join("~", toSign);
    byte[] toSignBytes = toSignJoined.getBytes(StandardCharsets.UTF_8);
    String algorithm = signatureAlgorithm.toLowerCase();

    if (algorithm.equalsIgnoreCase("ed25519")) {
      Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(decodedKey, 0);
      Ed25519Signer signer = new Ed25519Signer();
      signer.init(true, privateKey);
      signer.update(toSignBytes, 0, toSignBytes.length);
      byte[] signature = signer.generateSignature();
      tokens.add(String.format("Signature=%s", base64Encoder(signature)));
    } else if (algorithm.equalsIgnoreCase("sha256")) {
      String sha256 = "HmacSHA256";
      Mac mac = Mac.getInstance(sha256);
      SecretKeySpec secretKeySpec = new SecretKeySpec(decodedKey, sha256);
      mac.init(secretKeySpec);
      byte[] signature = mac.doFinal(toSignBytes);
      tokens.add(String.format("hmac=%s", Hex.toHexString(signature)));
    } else if (algorithm.equalsIgnoreCase("sha1")) {
      String sha1 = "HmacSHA1";
      Mac mac = Mac.getInstance(sha1);
      SecretKeySpec secretKeySpec = new SecretKeySpec(decodedKey, sha1);
      mac.init(secretKeySpec);
      byte[] signature = mac.doFinal(toSignBytes);
      tokens.add(String.format("hmac=%s", Hex.toHexString(signature)));
    } else {
      throw new Error(
          "Input Missing Error: `signatureAlgorithm` can only be one of `sha1`, `sha256` or "
              + "`ed25519`");
    }
    // The signed URL appended with the query parameters based on the
    // specified URL prefix and configuration.
    System.out.println(String.join("~", tokens));
  }

  // Returns a base64-encoded string compatible with Media CDN.
  // Media CDN uses URL-safe base64 encoding and strips off the padding at the
  // end.
  public static String base64Encoder(byte[] value) {
    byte[] encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(value);
    return new String(encodedBytes, StandardCharsets.UTF_8);
  }

  public static class Header {

    private String name;
    private String value;

    public Header(String name, String value) {
      this.name = name;
      this.value = value;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getValue() {
      return value;
    }

    public void setValue(String value) {
      this.value = value;
    }

    @Override
    public String toString() {
      return "Header{"
          + "name='" + name + '\''
          + ", value='" + value + '\''
          + '}';
    }
  }

}

다음 섹션에서는 토큰에 사용되는 필드에 대해 설명합니다.

필수 토큰 필드

다음 필드는 모든 토큰에 필요합니다.

  • Expires
  • 다음 중 하나:
    • PathGlobs
    • URLPrefix
    • FullPath
  • 다음 중 하나:
    • Signature
    • hmac

달리 지정되지 않은 한 매개변수 이름과 값은 대소문자를 구분합니다.

다음 표에서는 각 매개변수에 대해 설명합니다.

필드 이름/별칭 토큰 매개변수 서명 값

Expires

exp

유닉스 시간(1970-01-01T00:00:00Z)을 기준으로 경과한 정수 초입니다. Expires=EXPIRATION_TIME, 이후에는 토큰이 더 이상 유효하지 않습니다.

PathGlobs

paths, acl

액세스 권한을 부여할 최대 5개의 경로 세그먼트 목록입니다. 세그먼트는 쉼표(,) 또는 느낌표(!)로 구분할 수 있으며 둘 다 사용할 수는 없습니다.

PathGlobs는 별표(*) 및 물음표(?)를 사용하여 경로에서 와일드 카드를 지원합니다. 단일 별표(*) 문자는 pathMatchTemplate의 패턴 일치 문법과 달리 여러 개의 경로 세그먼트를 포함합니다.

세미콜론(;)을 사용하여 표시되는 경로 매개변수는 일치 비교 시 모호성을 일으키기 때문에 허용되지 않습니다.

따라서 URL에 다음 특수 문자가 포함되지 않도록 해야 합니다. ,!*?;

PathGlobs=PATHS
URLPrefix

원하는 지점까지 http:// 또는 https:// 프로토콜을 포함하는 웹 안전 base64 인코딩 URL입니다.

예를 들어 `https://example.com/foo/bar.ts`에서 유효한 URLPrefix 값은 `https://example.com`, `https://example.com/foo`, `https://example.com/foo/bar`입니다.

URLPrefix=BASE_64_URL_PREFIX
FullPath 없음. 토큰에 FullPath를 지정할 때는 서명 값에 지정한 경로를 복제하지 마세요. 토큰에서 = 없이 토큰 이름을 포함합니다. FullPath=FULL_PATH_TO_OBJECT
Signature 서명의 웹 보안 base64 인코딩 버전입니다. 해당 사항 없음
hmac HMAC 값의 웹 보안 base64 인코딩 버전입니다. 해당 사항 없음

PathGlobs 와일드 카드 문법

다음 표에서는 PathGlobs 와일드 카드 문법을 설명합니다.

연산자 일치 예시
*(별표) 정방향 슬래시(/) 문자를 포함하여 URL 경로에서 0개 이상의 문자와 일치합니다.
  • /videos/*/videos/로 시작하는 모든 경로와 일치합니다.
  • /videos/s*/4k/*/videos/s/4k//videos/s01/4k/main.m3u8과 일치합니다.
  • /manifests/*/4k/*/manifests/s01/4k/main.m3u8/manifests/s01/e01/4k/main.m3u8과 일치합니다. /manifests/4k/main.m3u8과는 일치하지 않습니다.
?(물음표) URL 경로의 단일 문자와 일치하며, 정방향 슬래시(/) 문자를 포함하지 않습니다. /videos/s?main.m3u8/videos/s1main.m3u8과 일치합니다. /videos/s01main.m3u8 또는 /videos/s/main.m3u8과는 일치하지 않습니다.

Globs는 URL 경로의 별표(*) 또는 정방향 슬래시(/)로 시작해야 합니다.

*/*는 모든 URL 경로와 일치하기 때문에 서명된 토큰에서 사용하지 않는 것이 좋습니다. 최대한의 보호를 위해서는 globs가 액세스를 부여하려는 콘텐츠와 일치하는지 확인합니다.

선택적 토큰 필드

달리 지정되지 않은 한 매개변수 이름과 값은 대소문자를 구분합니다.

다음 표에서는 매개변수 이름, 별칭, 선택적 매개변수의 세부정보에 대해 설명합니다.

필드 이름/별칭 매개변수 서명 값

Starts

st

유닉스 시간(1970-01-01T00:00:00Z) 이후의 정수 초입니다. Starts=START_TIME
IPRanges

이 URL이 웹 안전 base64 형식으로 유효한 CIDR 형식에서 최대 5개의 IPv4 및 IPv6 주소 목록입니다. 예를 들어 IP 범위를 "192.6.13.13/32,193.5.64.135/32"로 지정하려면 IPRanges=MTkyLjYuMTMuMTMvMzIsMTkzLjUuNjQuMTM1LzMy를 지정합니다.

클라이언트에 WAN 마이그레이션 위험이 있거나 애플리케이션 프런트엔드에 대한 네트워크 경로가 전송 경로와 다른 경우에는 IPRanges를 토큰에 포함하는 것이 유용하지 않을 수 있습니다. Media CDN은 서명된 요청에 속하지 않는 IP 주소로 연결될 때 HTTP 403 코드로 클라이언트를 거부합니다.

다음은 Media CDN이 HTTP 403 코드로 클라이언트를 거부할 수 있는 경우입니다.

  • 이중 스택(IPv4, IPv6) 환경
  • 연결 마이그레이션(WiFi-이동통신 및 이동통신-WiFi)
  • 이동통신사 게이트웨이 NAT(CGNAT 또는 CGN)를 사용하는 모바일 네트워크
  • 다중 경로 TCP(MPTCP)

이러한 모든 요소는 지정된 클라이언트가 동영상 재생 세션 중 비결정적 IP 주소를 갖도록 하는 데 기여할 수 있습니다. 접근이 허용된 후 클라이언트 IP 주소가 변경되고 클라이언트가 동영상 세그먼트를 재생 버퍼에 다운로드하려고 시도하면 Media CDN에서 HTTP 403이 수신됩니다.

IPRanges=BASE_64_IP_RANGES

SessionID

id

로그 분석 또는 재생 추적에 유용한 임의 문자열입니다.

부적합한 토큰을 만들지 않도록 %-encoded 또는 웹 안전 base64 인코딩 문자열을 사용합니다. 다음 문자는 토큰을 무효화하기 때문에 SessionID에 사용하지 않아야 합니다. "~", "&", " "(공백).

SessionID=SESSION_ID_VALUE

Data

data, payload

로그 분석에 유용한 임의 문자열입니다.

부적합한 토큰을 만들지 않도록 %-encoded 또는 웹 안전 base64 인코딩 문자열을 사용합니다. 다음 문자는 토큰을 무효화하기 때문에 Data에 사용하지 않아야 합니다. "~", "&", " "(공백).

data=DATA_VALUE
Headers 쉼표로 구분된 헤더 필드 이름 목록입니다. 헤더 이름은 요청에서 조회할 때 대소문자를 구분하지 않습니다. 서명 값의 헤더 이름은 대소문자를 구분합니다. 헤더가 누락된 경우에는 값이 빈 문자열입니다. 헤더 사본이 여러 개 있으면 쉼표로 연결됩니다. Headers=HEADER_1_NAME=HEADER_1_EXPECTED_VALUE, HEADER_2_NAME=HEADER_2_EXPECTED_VALUE

예시

다음 섹션에서는 토큰 생성 예시를 보여줍니다.

FullPath 사용 예시

FullPath 필드를 사용하는 다음 예시를 고려하세요.

  • 요청한 항목: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • 만료 시간: 160000000

서명 값은 다음과 같습니다.

Expires=160000000~FullPath=/tv/my-show/s01/e01/playlist.m3u8

토큰을 만들려면 Ed25519 서명 또는 대칭 키 HMAC를 사용하여 서명 값을 서명합니다.

다음은 서명 값에서 생성된 토큰 예시입니다.

Ed25519 서명

Expires=160000000~FullPath~Signature=SIGNATURE_OF_SIGNED_VALUE

여기서 SIGNATURE_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 ED25519 서명입니다.

대칭 키 HMAC

Expires=160000000~FullPath~hmac=HMAC_OF_SIGNED_VALUE

여기서 HMAC_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 대칭 키 HMAC입니다.

앞의 예시에서 FullPath는 토큰에 제공되지만 서명 값에 지정된 경로에서 값이 반복되지 않습니다. 이렇게 하면 토큰에서 요청을 복제하지 않고 요청의 전체 경로를 서명할 수 있습니다.

URLPrefix 사용 예시

URLPrefix 필드를 사용하는 다음 예시를 고려하세요.

  • 요청한 항목: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • 만료 시간: 160000000

서명 값은 다음과 같습니다.

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4

이전 예시에서는 요청된 항목에 대한 경로 http://example.com/tv/my-show/s01/e01/playlist.m3u8을 웹 안전 Base64 형식의 항목 경로 aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4로 바꿨습니다.

토큰을 만들려면 Ed25519 서명 또는 대칭 키 HMAC를 사용하여 서명 값을 서명합니다.

다음은 서명 값에서 생성된 토큰 예시입니다.

Ed25519 서명

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4~Signature=SIGNATURE_OF_SIGNED_VALUE

여기서 SIGNATURE_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 ED25519 서명입니다.

대칭 키 HMAC

Expires=160000000~URLPrefix=aHR0cDovL2V4YW1wbGUuY29tL3R2L215LXNob3cvczAxL2UwMS9wbGF5bGlzdC5tM3U4~hmac=HMAC_OF_SIGNED_VALUE

여기서 HMAC_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 대칭 키 HMAC입니다.

Headers 사용 예시

Headers 필드를 사용하는 다음 예시를 고려하세요.

  • 요청한 항목: http://example.com/tv/my-show/s01/e01/playlist.m3u8
  • 만료 시간: 160000000
  • PathGlobs 값: *
  • 예상 요청 헤더:
    • user-agent: browser
    • accept: text/html

서명 값은 다음과 같습니다.

Expires=160000000~PathGlobs=*~Headers=user-agent=browser,accept=text/html

토큰을 만들려면 Ed25519 서명 또는 대칭 키 HMAC를 사용하여 서명 값을 서명합니다.

다음은 서명 값에서 생성된 토큰 예시입니다.

Ed25519 서명

Expires=160000000~PathGlobs=*~Headers=user-agent,accept~Signature=SIGNATURE_OF_SIGNED_VALUE

여기서 SIGNATURE_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 ED25519 서명입니다.

대칭 키 HMAC

Expires=160000000~PathGlobs=*~Headers=user-agent,accept~hmac=HMAC_OF_SIGNED_VALUE

여기서 HMAC_OF_SIGNED_VALUE는 이전에 생성된 서명 값의 대칭 키 HMAC입니다.

앞의 예시에서 Headers=user-agent,accept는 토큰에 제공되지만 예상 헤더 값이 서명 값에서 반복되지 않습니다. 이렇게 하면 토큰에서 값을 복제하지 않고 특정 요청 헤더 키-값 쌍을 서명할 수 있습니다.