開啟M3U8標準加密改寫功能后,可以改寫HLS(HTTP Live Streaming)協(xié)議的M3U8文件(Media Playlist,媒體播放列表)。改寫成功后會在M3U8文件內(nèi)#EXT-X-KEY標簽后面增加加密參數(shù)(包括加密算法、密鑰URI地址和鑒權(quán)參數(shù)),客戶端收到被改寫的M3U8文件以后,將會使用帶鑒權(quán)參數(shù)的密鑰URI來發(fā)起請求,從CDN節(jié)點獲取到密鑰以后將會使用對應(yīng)的加密算法和密鑰來解密TS文件。即通過配置M3U8標準加密改寫功能,可以實現(xiàn)對HLS數(shù)據(jù)訪問過程的加密保護。

您可以參考以下內(nèi)容,詳細了解HLS(M3U8)標準加密改寫:

背景信息

HLS(HTTP Live Streaming的縮寫)是一個由蘋果公司提出的基于HTTP的流媒體網(wǎng)絡(luò)傳輸協(xié)議。HLS協(xié)議基于HTTP協(xié)議,客戶端按照順序使用HTTP協(xié)議下載存儲在服務(wù)器上的文件。HLS協(xié)議規(guī)定,視頻的封裝格式是TS(Transport Stream),除了TS視頻文件本身,還定義了用來控制播放的M3U8文件(文本文件)。HLS協(xié)議的工作原理是把整個視頻流分割成一個個小的TS格式視頻文件來傳輸,在開始一個流媒體會話時,客戶端會先下載一個包含TS文件URL地址的M3U8文件(相當于一個播放列表),給客戶端用于下載TS文件。

HLS基本字段
  • #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文件結(jié)束符。
  • #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 來獲取,密鑰請求回來以后存儲在本地,并用于解密后續(xù)下載的TS視頻文件。

技術(shù)原理

  1. 客戶端向CDN節(jié)點發(fā)起對M3U8文件的訪問請求,例如:http://example.com/media/index.m3u8?MtsHlsUriToken=xxx
  2. CDN節(jié)點對客戶端的訪問請求進行校驗,校驗通過。
  3. CDN節(jié)點從源站下載原始M3U8文件,并緩存原始M3U8文件。
  4. CDN節(jié)點對原始M3U8文件的#EXT-X-KEY標簽進行改寫,增加加密方式、密鑰URI和鑒權(quán)參數(shù),例如:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/video.key?MtsHlsUriToken=xxx"
  5. CDN節(jié)點將改寫后的M3U8文件返回給客戶端。
  6. 客戶端解析改寫后的M3U8文件,拿到密鑰URI地址https://example.com/video.key?MtsHlsUriToken=xxx,并發(fā)起訪問請求。
  7. CDN節(jié)點收到客戶端請求,鑒權(quán)通過之后,將key文件返回給客戶端。
  8. 客戶端繼續(xù)解析改寫后的M3U8文件,從CDN節(jié)點下載其中的TS視頻文件。
  9. 客戶端使用key文件內(nèi)的密鑰和前面#EXT-X-KEY標簽內(nèi)定義的加密算法來解密TS視頻文件。

適用場景

HLS協(xié)議采用M3U8文件來告知客戶端視頻文件播放列表,客戶端拿到M3U8文件以后就可以直接播放視頻,為了避免源站的視頻文件被非授權(quán)客戶端訪問,需要對HLS協(xié)議使用的TS視頻文件做加密,對TS視頻文件做了加密以后,還需要告知客戶端解密方法,這里就可以通過配置M3U8標準加密改寫功能,通過#EXT-X-KEY標簽來告知客戶端加密算法、密鑰URI和鑒權(quán)key。

使用方法

  1. 在視頻點播控制臺開啟HLS標準加密參數(shù)透傳
    詳細步驟請參見HLS標準加密參數(shù)透傳
  2. 客戶端攜帶MtsHlsUriToken參數(shù)向CDN節(jié)點發(fā)起對M3U8文件的訪問請求。
    其中,MtsHlsUriToken需要您自行搭建令牌服務(wù),頒發(fā)用戶令牌(即生成MtsHlsUriToken)。
    下述代碼所生成的Token即是MtsHlsUriToken。下述Java示例代碼中,您需要按實際情況變更的參數(shù)如下:
    參數(shù)傳入值
    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生成方式,無需以下參數(shù)
        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));   //驗證解密部分
    
        }
        /**
         * 根據(jù)傳遞的參數(shù)生成令牌
         * 說明:
         *  1、參數(shù)可以是業(yè)務(wù)方的用戶ID、播放終端類型等信息
         *  2、調(diào)用令牌接口時生成令牌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), "_");
            //設(shè)置30S后,該token過期,過期時間可以自行調(diào)整
            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的使用次數(shù)
            saveToken(token);
            return token;
        }
    
        /**
         * 驗證token的有效性
         * 說明:
         *  1、解密接口在返回播放密鑰前,需要先校驗Token的合法性和有效性
         *  2、強烈建議同時校驗Token的過期時間以及Token的有效使用次數(shù)
         * @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的有效性,業(yè)務(wù)方可自行實現(xiàn)
            TokenInfo dbToken = getToken(token);
            //判斷是否已經(jīng)使用過該token
            if (dbToken == null || dbToken.useCount > 0) {
                return false;
            }
            //獲取到業(yè)務(wù)屬性信息,用于校驗
            String businessInfo = base.substring(0, base.lastIndexOf("_"));
            String[] items = businessInfo.split("_");
            //校驗業(yè)務(wù)信息的合法性,業(yè)務(wù)方實現(xiàn)
            return validateInfo(items);
        }
        /**
         * 保存Token到DB
         * 業(yè)務(wù)方自行實現(xiàn)
         *
         * @param token
         */
        public void saveToken(String token) {
            //TODO 存儲Token
        }
        /**
         * 查詢Token
         * 業(yè)務(wù)方自行實現(xiàn)
         *
         * @param token
         */
        public TokenInfo getToken(String token) {
            //TODO 從DB獲取Token信息,用于校驗有效性和合法性
            return null;
        }
        /**
         * 校驗業(yè)務(wù)信息的有效性,業(yè)務(wù)方可自行實現(xiàn)
         *
         * @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信息,業(yè)務(wù)方可提供更多信息,這里僅給出示例供參考
         */
        class TokenInfo {
            //Token的有效使用次數(shù),分布式環(huán)境需要注意同步修改問題
            int useCount;
            //token內(nèi)容
            String token;
        }}
                            
  3. CDN節(jié)點收到客戶端請求后,鑒權(quán)通過則解密播放文件。
    若上述步驟二中生成的MtsHlsUriToken參數(shù)值為test,則當CDN解密播放時,會將MtsHlsUriToken=test追加到M3U8文件中#EXT-X-KEY標簽的URI末尾。

    具體的鑒權(quán)校驗邏輯您需要自行實現(xiàn),可以參考播放HLS標準加密視頻中開啟M3U8標準加密改寫方式的解密服務(wù)的示例代碼。