開啟M3U8標準加密改寫功能後,可以改寫HLS(HTTP Live Streaming)協議的M3U8檔案(Media Playlist,媒體播放清單)。改寫成功後會在M3U8檔案內#EXT-X-KEY
標籤後面增加加密參數(包括密碼編譯演算法、密鑰URI地址和鑒權參數),用戶端收到被改寫的M3U8檔案以後,將會使用帶鑒權參數的密鑰URI來發起請求,從CDN節點擷取到密鑰以後將會使用對應的密碼編譯演算法和密鑰來解密TS檔案。即通過配置M3U8標準加密改寫功能,可以實現對HLS資料訪問過程的加密保護。
背景資訊
HLS(HTTP Live Streaming的縮寫)是一個由蘋果公司提出的基於HTTP的流媒體網路傳輸協議。HLS協議基於HTTP協議,用戶端按照順序使用HTTP協議下載儲存在伺服器上的檔案。HLS協議規定,視頻的封裝格式是TS(Transport Stream),除了TS視頻檔案本身,還定義了用來控制播放的M3U8檔案(文字檔)。HLS協議的工作原理是把整個視頻流分割成一個個小的TS格式視頻檔案來傳輸,在開始一個流媒體會話時,用戶端會先下載一個包含TS檔案URL地址的M3U8檔案(相當於一個播放清單),給用戶端用於下載TS檔案。
#EXTM3U
:M3U8檔案頭,必須放在第一行。EXT-X-MEDIA-SEQUENCE
:第一個TS分區的序號,一般情況下是0,但是在直播情境下,這個序號標識直播段的起始位置;#EXT-X-MEDIA-SEQUENCE:0
。#EXT-X-TARGETDURATION
:每個分區TS的最大的時間長度;#EXT-X-TARGETDURATION:10
,表示每個分區的最大時間長度是10秒。#EXT-X-ALLOW-CACHE
:是否允許cache,#EXT-X-ALLOW-CACHE:YES
、#EXT-X-ALLOW-CACHE:NO
,預設情況下是YES。#EXT-X-ENDLIST
:M3U8檔案結束符。#EXTINF
:extra info,分區TS的資訊,如時間長度,頻寬等;一般情況下是#EXTINF:<duration>,[<title>]
後面可以跟其他的資訊,逗號之前是當前分區的TS時間長度。分區時間長度要小於#EXT-X-TARGETDURATION
定義的值。#EXT-X-VERSION
:M3U8版本號碼。#EXT-X-DISCONTINUITY
:該標籤表明其前一個切片與下一個切片之間存在中斷。#EXT-X-PLAYLIST-TYPE
:表明流媒體類型。#EXT-X-KEY
:是否加密解析。例如:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/video.key?token=xxx"
密碼編譯演算法是AES-128,密鑰通過請求https://example.com/video.key?token=xxx
來擷取,密鑰請求回來以後儲存在本地,並用於解密後續下載的TS視頻檔案。
技術原理
- 用戶端向CDN節點發起對M3U8檔案的訪問請求,例如:
http://example.com/media/index.m3u8?MtsHlsUriToken=xxx
。 - CDN節點對用戶端的訪問請求進行校正,校正通過。
- CDN節點從來源站點下載原始M3U8檔案,並緩衝原始M3U8檔案。
- CDN節點對原始M3U8檔案的
#EXT-X-KEY
標籤進行改寫,增加加密方式、密鑰URI和鑒權參數,例如:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/video.key?MtsHlsUriToken=xxx"
。 - CDN節點將改寫後的M3U8檔案返回給用戶端。
- 用戶端解析改寫後的M3U8檔案,拿到密鑰URI地址
https://example.com/video.key?MtsHlsUriToken=xxx
,並發起訪問請求。 - CDN節點收到用戶端請求,鑒權通過之後,將key檔案返回給用戶端。
- 用戶端繼續解析改寫後的M3U8檔案,從CDN節點下載其中的TS視頻檔案。
- 用戶端使用key檔案內的密鑰和前面
#EXT-X-KEY
標籤內定義的密碼編譯演算法來解密TS視頻檔案。
適用情境
HLS協議採用M3U8檔案來告知用戶端視頻檔案播放清單,用戶端拿到M3U8檔案以後就可以直接播放視頻,為了避免來源站點的視頻檔案被非授權用戶端訪問,需要對HLS協議使用的TS視頻檔案做加密,對TS視頻檔案做了加密以後,還需要告知用戶端解密方法,這裡就可以通過配置M3U8標準加密改寫功能,通過#EXT-X-KEY
標籤來告知用戶端密碼編譯演算法、密鑰URI和鑒權key。
使用方法
- 在ApsaraVideo for VOD控制台開啟HLS標準加密參數透傳。詳細步驟請參見HLS標準加密參數透傳。
- 用戶端攜帶MtsHlsUriToken參數向CDN節點發起對M3U8檔案的訪問請求。其中,MtsHlsUriToken需要您自行搭建令牌服務,頒發使用者令牌(即產生MtsHlsUriToken)。下述代碼所產生的Token即是MtsHlsUriToken。下述Java範例程式碼中,您需要按實際情況變更的參數如下:
參數 傳入值 ENCRYPT_KE 加密Key,為使用者自訂字串,長度為16、24或32位。 INIT_VECTOR 加密位移量,為使用者自訂字串,長度為16位,不能含有特殊字元。 import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Arrays; public class PlayToken { //非AES產生方式,無需以下參數 private static String ENCRYPT_KEY = ""; //加密Key,為使用者自訂的字串,長度為16、24或32位 private static String INIT_VECTOR = ""; //加密位移量,為使用者自訂字串,長度為16位,不能含有特殊字元 public static void main(String[] args) throws Exception { String serviceId = "12"; PlayToken playToken = new PlayToken(); String aesToken = playToken.generateToken(serviceId); //System.out.println("aesToken " + aesToken); //System.out.println(playToken.validateToken(aesToken)); //驗證解密部分 } /** * 根據傳遞的參數產生令牌 * 說明: * 1、參數可以是業務方的使用者ID、播放終端類型等資訊 * 2、調用令牌介面時產生令牌Token * @param args * @return */ public String generateToken(String... args) throws Exception { if (null == args || args.length <= 0) { return null; } String base = StringUtils.join(Arrays.asList(args), "_"); //設定30S後,該token到期,到期時間可以自行調整 long expire = System.currentTimeMillis() + 30000L; base += "_" + expire; //自訂字串,base的長度為16位字元(此例中,時間戳記佔13位,底線(_)佔1位,則還需傳入2位字元。實際配置時也可按需全部更改,最終保證base為16、24或32位字串即可。) //產生token String token = encrypt(base, ENCRYPT_KEY); //arg1為要加密的自訂字串,arg2為加密Key //儲存token,用於解密時校正token的有效性,例如:到期時間、token的使用次數 saveToken(token); return token; } /** * 驗證token的有效性 * 說明: * 1、解密介面在返回播放密鑰前,需要先校正Token的合法性和有效性 * 2、強烈建議同時校正Token的到期時間以及Token的有效使用次數 * @param token * @return * @throws Exception */ public boolean validateToken(String token) throws Exception { if (null == token || "".equals(token)) { return false; } String base = decrypt(token,ENCRYPT_KEY); //arg1為解密字串,arg2為解密Key //先校正token的有效時間 Long expireTime = Long.valueOf(base.substring(base.lastIndexOf("_") + 1)); System.out.println("時間校正:" + expireTime); if (System.currentTimeMillis() > expireTime) { return false; } //從DB擷取token資訊,判斷token的有效性,業務方可自行實現 TokenInfo dbToken = getToken(token); //判斷是否已經使用過該token if (dbToken == null || dbToken.useCount > 0) { return false; } //擷取到業務屬性資訊,用於校正 String businessInfo = base.substring(0, base.lastIndexOf("_")); String[] items = businessInfo.split("_"); //校正商務資訊的合法性,業務方實現 return validateInfo(items); } /** * 儲存Token到DB * 業務方自行實現 * * @param token */ public void saveToken(String token) { //TODO 儲存Token } /** * 查詢Token * 業務方自行實現 * * @param token */ public TokenInfo getToken(String token) { //TODO 從DB擷取Token資訊,用於校正有效性和合法性 return null; } /** * 校正商務資訊的有效性,業務方可自行實現 * * @param infos * @return */ public boolean validateInfo(String... infos) { //TODO 校正資訊的有效性,例如UID是否有效等 return true; } /** * AES加密產生Token * * @param encryptStr 要加密的字串 * @param encryptKey 加密Key * @return * @throws Exception */ public String encrypt(String encryptStr, String encryptKey) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(encryptKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, e); byte[] encrypted = cipher.doFinal(encryptStr.getBytes()); return Base64.encodeBase64String(encrypted); } /** * AES解密token * * @param encryptStr 解密字串 * @param decryptKey 解密Key * @return * @throws Exception */ public String decrypt(String encryptStr, String decryptKey) throws Exception { IvParameterSpec e = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(decryptKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, e); byte[] encryptByte = Base64.decodeBase64(encryptStr); byte[] decryptByte = cipher.doFinal(encryptByte); return new String(decryptByte); } /** * Token資訊,業務方可提供更多資訊,這裡僅給出樣本供參考 */ class TokenInfo { //Token的有效使用次數,分布式環境需要注意同步修改問題 int useCount; //token內容 String token; }}
- CDN節點收到用戶端請求後,鑒權通過則解密播放檔案。若上述步驟二中產生的
MtsHlsUriToken
參數值為test
,則當CDN解密播放時,會將MtsHlsUriToken=test
追加到M3U8檔案中#EXT-X-KEY
標籤的URI末尾。具體的鑒權校正邏輯您需要自行實現,可以參考播放HLS標準加密視頻中開啟M3U8標準加密改寫方式的解密服務的範例程式碼。