創建一個 JSON 網絡令牌(JWT)

在本主題中,您將了解如何創建可與 Brightcove 播放限制一起使用的 JSON Web 令牌 (JWT)。

簡介

要在訪問您的視頻庫時添加額外級別的保護,或對您的內容應用用戶級別限制,您可以通過JSON Web Token (JWT)通過調用 Brightcove Playback API。

如果您不熟悉JWT's , 查看以下內容:

工作流程

創建一個JSON Web Token (JWT)並在 Brightcove 註冊,請按照以下步驟操作:

  1. 產生公開私密金鑰組
  2. 向布萊特灣註冊公開金鑰
  3. 建立JSON Web Token
  4. 測試播放

產生公開私密金鑰組

您(發布者)將生成一對公私密鑰並將公鑰提供給 Brightcove。您將使用私鑰對令牌進行簽名。私鑰不與 Brightcove 共享。

有很多方法可以產生公開私密金鑰組。以下是一些範例:

bash 腳本示例:

產生金鑰配對的範例指令碼:

#!/bin/bash
set -euo pipefail

NAME=${1:-}
test -z "${NAME:-}" && NAME="brightcove-playback-auth-key-$(date +%s)"
mkdir "$NAME"

PRIVATE_PEM="./$NAME/private.pem"
PUBLIC_PEM="./$NAME/public.pem"
PUBLIC_TXT="./$NAME/public_key.txt"

ssh-keygen -t rsa -b 2048 -m PEM -f "$PRIVATE_PEM" -q -N ""
openssl rsa -in "$PRIVATE_PEM" -pubout -outform PEM -out "$PUBLIC_PEM" 2>/dev/null
openssl rsa -in "$PRIVATE_PEM" -pubout -outform DER | base64 > "$PUBLIC_TXT"

rm "$PRIVATE_PEM".pub

echo "Public key to saved in $PUBLIC_TXT"

執行指令碼:

$ bash keygen.sh
使用範例Go

使用Go編程語言生成密鑰對的示例:

package main
  
  import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "path"
    "strconv"
    "time"
  )
  
  func main() {
    var out string
  
    flag.StringVar(&out, "output-dir", "", "Output directory to write files into")
    flag.Parse()
  
    if out == "" {
      out = "rsa-key_" + strconv.FormatInt(time.Now().Unix(), 10)
    }
  
    if err := os.MkdirAll(out, os.ModePerm); err != nil {
      panic(err.Error())
    }
  
    priv, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
      panic(err.Error())
    }
  
    privBytes := x509.MarshalPKCS1PrivateKey(priv)
  
    pubBytes, err := x509.MarshalPKIXPublicKey(priv.Public())
    if err != nil {
      panic(err.Error())
    }
  
    privOut, err := os.OpenFile(path.Join(out, "private.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
      panic(err.Error())
    }
  
    if err := pem.Encode(privOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}); err != nil {
      panic(err.Error())
    }
  
    pubOut, err := os.OpenFile(path.Join(out, "public.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
      panic(err.Error())
    }
  
    if err := pem.Encode(pubOut, &pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}); err != nil {
      panic(err.Error())
    }
  
    var pubEnc = base64.StdEncoding.EncodeToString(pubBytes)
  
    var pubEncOut = path.Join(out, "public_key.txt")
    if err := ioutil.WriteFile(pubEncOut, []byte(pubEnc+"\n"), 0600); err != nil {
      panic(err.Error())
    }
  
    fmt.Println("Public key saved in " + pubEncOut)
  }
  

使用 node.js 的範例

使用 node.js 生成密鑰對的示例:

var crypto = require("crypto");
  var fs = require("fs");
  
  var now = Math.floor(new Date() / 1000);
  var dir = "rsa-key_" + now;
  fs.mkdirSync(dir);
  
  crypto.generateKeyPair(
    "rsa",
    {modulusLength: 2048},
    (err, publicKey, privateKey) => {
      fs.writeFile(
        dir + "/public.pem",
        publicKey.export({ type: "spki", format: "pem" }),
        err => {}
      );
      fs.writeFile(
        dir + "/public_key.txt",
        publicKey.export({ type: "spki", format: "der" }).toString("base64") +
          "\n",
        err => {}
      );
      fs.writeFile(
        dir + "/private.pem",
        privateKey.export({ type: "pkcs1", format: "pem" }),
        err => {}
      );
    }
  );
  
  console.log("Public key saved in " + dir + "/public_key.txt");

註冊公開金鑰

您擁有私鑰,您將使用它來生成簽名令牌。您將與 Brightcove 共享公鑰以驗證您的令牌。密鑰 API 允許您使用 Brightcove 註冊您的公鑰。

有關 API 的詳細信息,請參閱 使用身份驗證 API 文檔。

建立JSON Web Token

發行者創建一個JSON Web Token(JWT)。使用 SHA-256 雜湊演算法 (在 JWT 規格中識別為 " RS256 」) 使用 RSA 演算法簽署該令牌不會支援其他 JWT 演算法。

JSON Web Token claims將使用標準的一部分,以及 Brightcove 定義的一些私人索賠。您將創建一個用您的私鑰JSON Web Token簽名。

靜態 URL 傳遞的宣告

以下聲明可用於 Brightcove 的靜態 URL 交付

宣稱 類型 必填 描述
accid 字串 擁有播放內容的帳號 ID
iat 整數 這個令牌發出的時間,以秒為單位,自紀元
exp 整數 這個令牌的時間將不再有效,以秒為單位,自紀元以來。必須不超過 30 天iat
drules 串[] 要應用的傳送規則操作 ID 列表。有關詳細信息,請參閱 實施交付規則 文檔。
如果config_id查詢參數也已設置,它將被忽略,因為此聲明會覆蓋它。
conid 字串 如果存在,此令牌將僅授權特定的 Video Cloud 視頻 ID。這可以是 DRM/HLSe 流或非 DRM 資產。

必須是有效的視頻 ID。請注意,不支持參考 ID。
pro 字串 在單一視訊可用多個情況下,指定保護類型。

值:
  • 「」(預設為清晰內容)
  • 「第 128」
  • 「威德維恩」
  • 「播放就緒」
  • 「公平遊戲」
vod 物件 包含視訊隨選視訊的特定組態選項。
vod.ssai 字串 您的伺服器端廣告插入 (SSAI) 組態識別碼。擷取 HLS 或破折號 VMAP 需要此宣告。

以下是您可能使用的JSON Web Token(JWT)聲明的示例:

{
// account id: JWT is only valid for this accounts
"accid":"4590388311111",
// issued at: timestamp when the JWT was created
"iat":1575484132,
// expires: timestamp when JWT expires
"exp":1577989732,
// drules: list of delivery rule IDs to be applied
"drules": ["0758da1f-e913-4f30-a587-181db8b1e4eb"],
// content id: JWT is only valid for video ID
"conid":"5805807122222",
// protection: specify a protection type in the case where multiple are available for a single video
"pro":"aes128",
// VOD specific configuration options
"vod":{
// SSAI configuration to apply
"ssai":"efcc566-b44b-5a77-a0e2-d33333333333"
}
}

播放限制索賠

以下聲明可與 Brightcove 播放限制一起使用。作為播放限制的一部分,您可以執行以下操作:

功能 宣稱 類型 功能所需 僅限 DRM 描述
一般 accid 字串 擁有播放內容的帳號 ID
iat 整數 這個令牌發出的時間,以秒為單位,自紀元
exp 整數 不是必需的,但強烈推薦。

此令牌不再有效的時間,自紀元以來的秒數。必須不超過 30 天iat
nbf 整數 此令牌開始生效的時間,自紀元以來的秒數。
如果未指定,令牌將立即可用。
播放權限 prid 字串 A playback_rights_id用於覆蓋此視頻目錄中設置的 ID

該字段未經驗證

tags 陣列 < 字串 > 如果存在,此令牌僅對具有列出的標籤值的視頻有效。只有這些視頻才有權播放。
vids 陣列 < 字串 > 如果存在,此令牌將只授權獲取一組視頻 ID 的許可證。

許可證密鑰保護 ua 字串 如果存在,此令牌將僅對來自此用戶代理的請求有效。

該字段不必遵循任何特定格式。
你必須有許可證密鑰保護啟用。
conid 字串 如果存在,此權杖只會授權擷取特定 Video Cloud 視訊 ID 的授權。

必須是有效的視頻 ID
你必須有許可證密鑰保護啟用。
maxip 整數 如果存在,則此令牌將只能由此數量的不同 IP 地址使用。

必需的用於會話跟踪;僅限 HLSe (AES-128)
你必須有許可證密鑰保護啟用。
maxu 整數 如果存在,此令牌將僅對此數量的許可請求有效。

  • 對於 HLSE,播放器在播放視訊時會提出多個要求,通常每個節目一次。maxu必須設定足夠高,才能解決這些額外的要求。
必需的用於會話跟踪;僅限 HLSe (AES-128)
你必須有許可證密鑰保護啟用。
並發流 uid 字串 最終檢視器的使用者 ID。該字段用於關聯多個會話以實施流並發。

您可以使用任意 ID(最多 64 個字符,限於 AZ、az、0-9 和 =/、@_.+-)。但是,根據您的用例,Brightcove 建議使用用戶標識符來跟踪每個用戶的會話,或者使用帳戶標識符來跟踪每個付費帳戶的會話。

必需的對於會話並發
climit 整數 包括此字段後,它將啟用流並發檢查以及許可證續訂請求。此值表示允許並行觀察者的數目。

必需的對於會話並發
cbeh 字串 將值設置為BLOCK_NEW啟用並發流限制以在達到最大流數時阻止任何新請求,即使來自同一用戶也是如此。

將值設置為BLOCK_NEW_USER當達到最大流數時,僅阻止來自新用戶的任何新請求。

當達到最大流數時,默認值將阻止最早的流。
sid 字串 通過指定當前流的會話ID,您可以控制如何定義會話。默認情況下,會話定義為用戶代理(瀏覽器)+ IP地址+視頻ID。

比如可以把session的定義放寬到IP地址+視頻ID

裝置限制 uid 字串 最終檢視器的使用者 ID。該字段用於關聯多個會話以實施流並發。

您可以使用任意 ID(最多 64 個字符,限於 AZ、az、0-9 和 =/、@_.+-)。但是,根據您的用例,Brightcove 建議使用用戶標識符來跟踪每個用戶的會話,或者使用帳戶標識符來跟踪每個付費帳戶的會話。

必需的用於設備註冊
dlimit 整數 包含此欄位時,它會控制可以與指定的使用者 ( uid ) 關聯的裝置數目。值必須是 > 0

如果在稍後的要求中捨棄該dlimit值,先前允許的裝置將繼續運作。

範例:如果值設定為3,則使用者可以在裝置 A、B 和 C 上播放 (所有將被允許)。嘗試在裝置 D 上播放將被拒絕。

如果將值更改為1,用戶仍然可以在A,B和C的所有3種設備上播放,除非通過使用以下命令管理設備來手動撤消設備播放權限API

必需的用於設備註冊
傳送規則 drules 串[] 要應用的傳送規則操作 ID 列表。有關詳細信息,請參閱 實施交付規則 文檔。

按等級索賠

有幾個安全包可用於播放限制。有關詳細信息,請參見概述:Brightcove 播放限制文檔。

以下是每個播放限制包的可用聲明:

功能 理賠 安全等級 1 安全等級 2 安全等級 3
一般
我在
exp
nbf
播放權 [1] 驕傲
標籤
西元
許可證密鑰保護 ua
錐度
最大的
最大
並發流 uid
極限
cbeh
資料庫
通用並發流 uid
極限
資料庫
設備註冊 uid
限制

產生權杖

庫通常可用於生成 JWT 令牌。有關詳細信息,請參閱JSON Web Tokens網站。

一個 時代 & Unix 時間戳轉換工具 在處理時間字段時可能會有幫助。

bash 腳本示例:

生成 JWT 令牌的示例腳本:

#! /usr/bin/env bash
# Static header fields.
HEADER='{
	"type": "JWT",
	"alg": "RS256"
}'

payload='{
	"accid": "{your_account_id}"
}'

# Use jq to set the dynamic `iat` and `exp`
# fields on the payload using the current time.
# `iat` is set to now, and `exp` is now + 1 hour. Note: 3600 seconds = 1 hour
PAYLOAD=$(
	echo "${payload}" | jq --arg time_str "$(date +%s)" \
	'
	($time_str | tonumber) as $time_num
	| .iat=$time_num
	| .exp=($time_num + 60 * 60)
	'
)

function b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; }

function rs_sign() { openssl dgst -binary -sha256 -sign playback-auth-keys/private.pem ; }

JWT_HDR_B64="$(echo -n "$HEADER" | b64enc)"
JWT_PAY_B64="$(echo -n "$PAYLOAD" | b64enc)"
UNSIGNED_JWT="$JWT_HDR_B64.$JWT_PAY_B64"
SIGNATURE=$(echo -n "$UNSIGNED_JWT" | rs_sign | b64enc)

echo "$UNSIGNED_JWT.$SIGNATURE"

執行指令碼:

$ bash jwtgen.sh

使用範例Go

以下是一個引用Go實現(作為 cli 工具)的示例,用於在不使用任何第三方庫的情況下生成令牌:

package main

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

// Header is the base64UrlEncoded string of a JWT header for the RS256 algorithm
const RSAHeader = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"

// Header is the base64UrlEncoded string of a JWT header for the EC256 algorithm
const ECHeader = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"

// Claims represents constraints that should be applied to the use of the token
type Claims struct {
	Iat   float64 `json:"iat,omitempty"`   // Issued At
	Exp   float64 `json:"exp,omitempty"`   // Expires At
	Accid string  `json:"accid,omitempty"` // Account ID
	Conid string  `json:"conid,omitempty"` // Content ID
	Maxu  float64 `json:"maxu,omitempty"`  // Max Uses
	Maxip float64 `json:"maxip,omitempty"` // Max IPs
	Ua    string  `json:"ua,omitempty"`    // User Agent
}

func main() {
	var key, algorithm string

	c := Claims{Iat: float64(time.Now().Unix())}

	flag.StringVar(&key, "key", "", "Path to private.pem key file")
	flag.StringVar(&c.Accid, "account-id", "", "Account ID")
	flag.StringVar(&c.Conid, "content-id", "", "Content ID (eg, video_id or live_job_id)")
	flag.Float64Var(&c.Exp, "expires-at", float64(time.Now().AddDate(0, 0, 1).Unix()), "Epoch timestamp (in seconds) for when the token should stop working")
	flag.Float64Var(&c.Maxu, "max-uses", 0, "Maximum number of times the token is valid for")
	flag.Float64Var(&c.Maxip, "max-ips", 0, "Maximum number of unique IP addresses the token is valid for")
	flag.StringVar(&c.Ua, "user-agent", "", "User Agent that the token is valid for")
	flag.StringVar(&algorithm, "algo", "", "Key algorithm to use for signing. Valid: ec256, rsa256")
	flag.Parse()

	if key == "" {
		fmt.Printf("missing required flag: -key\n\n")
		flag.Usage()
		os.Exit(1)
	}

	if algorithm == "" {
		fmt.Printf("missing required flag: -algo\n\n")
		flag.Usage()
		os.Exit(2)
	}

	if algorithm != "rsa256" && algorithm != "ec256" {
		fmt.Printf("missing valid value for -algo flag. Valid: rsa256, ec256\n\n")
		flag.Usage()
		os.Exit(3)
	}

	if c.Accid == "" {
		fmt.Printf("missing required flag: -account-id\n\n")
		flag.Usage()
		os.Exit(4)
	}

	bs, err := json.Marshal(c)
	if err != nil {
		fmt.Println("failed to marshal token to json", err)
		os.Exit(5)
	}

	kbs, err := ioutil.ReadFile(key)
	if err != nil {
		fmt.Println("failed to read private key", err)
		os.Exit(6)
	}

	if algorithm == "rsa256" {
		processRSA256(kbs, bs)
	} else {
		processEC256(kbs, bs)
	}
}

func processRSA256(kbs, bs []byte) {
	block, _ := pem.Decode(kbs)
	if block == nil {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(7)
	}

	if block.Type != "RSA PRIVATE KEY" {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(8)
	}

	pKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		fmt.Println("failed to parse rsa private key", err)
		os.Exit(9)
	}

	message := RSAHeader + "." + base64.RawURLEncoding.EncodeToString(bs)

	hash := crypto.SHA256
	hasher := hash.New()
	_, _ = hasher.Write([]byte(message))
	hashed := hasher.Sum(nil)

	r, err := rsa.SignPKCS1v15(rand.Reader, pKey, hash, hashed)
	if err != nil {
		fmt.Println("failed to sign token", err)
		os.Exit(10)
	}

	sig := strings.TrimRight(base64.RawURLEncoding.EncodeToString(r), "=")

	fmt.Println(message + "." + sig)
}

func processEC256(kbs, bs []byte) {
	block, _ := pem.Decode(kbs)
	if block == nil {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(7)
	}

	if block.Type != "EC PRIVATE KEY" {
		fmt.Println("failed to decode PEM block containing private key")
		os.Exit(8)
	}

	pkey, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		fmt.Println("failed to parse ec private key", err)
		os.Exit(9)
	}

	message := ECHeader + "." + base64.RawURLEncoding.EncodeToString(bs)
	hash := sha256.Sum256([]byte(message))

	r, s, err := ecdsa.Sign(rand.Reader, pkey, hash[:])
	if err != nil {
		fmt.Println("failed to sign token", err)
		os.Exit(10)
	}

	curveBits := pkey.Curve.Params().BitSize

	keyBytes := curveBits / 8
	if curveBits%8 > 0 {
		keyBytes++
	}

	rBytes := r.Bytes()
	rBytesPadded := make([]byte, keyBytes)
	copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)

	sBytes := s.Bytes()
	sBytesPadded := make([]byte, keyBytes)
	copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)

	out := append(rBytesPadded, sBytesPadded...)

	sig := base64.RawURLEncoding.EncodeToString(out)
	fmt.Println(message + "." + sig)
}

結果

以下是一個使用 https://JWT.io 指定完整的聲明集的解碼令牌的示例:

標頭:

{
  "alg": "RS256",
  "type": "JWT"
}

有效載荷:

{
  "accid": "1100863500123",
  "conid": "51141412620123",
  "exp": 1554200832,
  "iat": 1554199032,
  "maxip": 10,
  "maxu": 10,
  "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}

測試播放

雖然不是必要的,但您可能想要在設定播放器之前先測試視訊播放。

靜態 URL 傳送

要求播放:

curl -X GET \
https://edge.api.brightcove.com/playback/v1/accounts/{account_id}/videos/{video_id}/master.m3u8?bcov_auth={jwt}

有關靜態 URL 端點的列表,請參閱 靜態 URL 交付 文檔。

播放限制

要求播放:

curl -X GET \
-H 'Authorization: Bearer {JWT}' \
https://edge-auth.api.brightcove.com/playback/v1/accounts/{your_account_id}/videos/{your_video_id}