m3u8收费视频防盗加密

3,041 阅读3分钟

1 场景

视频采用m3u8 ts切片方式,采用切片默认的aes-128加密方式不安全,key值也是16位明文的,随便下个video.js插件就可以破解(其它播放插件也可以),让后端做来源判断或者加请求标识个人感觉安全系数也不是很大。

2 m3u8视频防盗方案

  • 方式一: 后端对m3u8返回内容进行加密处理,前端请求m3u8时可以包括加SIGN验证,根据视频id加请求参数验证等,以便后端判断请求是否给你返回内容。

  • 关于方式一解密(加解密方案前后端自己定即可,以下均是修改插件源码并以标明位置函数,实测前后端以调通)

  //插件:video.js - 7.0.5(此版本默认不包含hls)
  
    //处理请求添加字段证明身份
    function _createXHR(options) {
      ...
        var failureResponse = {
            body: undefined,
            headers: {},
            statusCode: 0,
            method: method,
            url: uri,
            rawRequest: xhr
        };
        
        // 自定义 - 开始
        if (options.uri.indexOf('自定义地址') > -1) {
            /**
             * 
             * 加密算法得出加密值
             * 
             */
            headers = { "SIGN": 加密值, 'Content-Type': 'application/x-www-form-urlencoded' };
            method = "POST";
        } else {
            method = xhr.method = options.method || "GET";
            headers = xhr.headers = options.headers || {};
        }
        // 自定义 - 结束

        if ("json" in options && options.json !== false) {
            isJson = true;
            headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
            if (method !== "GET" && method !== "HEAD") {
                headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
                body = JSON.stringify(options.json === true ? body : options.json);
            }
        }
      ...
    }
  
  
    //插件:videojs-contrib-hls.js - 5.9.0
  
    var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) {
        ...
            if (errorObj) {
                return finishProcessingFn(errorObj, segment);
            }
                            
            // 解密二次加密后的key - 开始
            if(请求key返回的正确code值){
                if (response.key !== "") {
                    /**
                     * 
                     * 咔咔咔写自己的解密
                     * 
                     */

                    var view = new DataView(解密得到的值);
                    segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
                    return finishProcessingFn(null, segment);
                }
            }else{
                // 用于初始化判断key长度
                if (response.byteLength !== 16) {
                    return finishProcessingFn({
                        status: request.status,
                        message: 'Invalid HLS key at URL: ' + request.uri,
                        code: REQUEST_ERRORS.FAILURE,
                        xhr: request
                    }, segment);
                }
            }
            // 解密二次加密后的key - 结束

            // 将原有的注释
            // var view = new DataView(response);
            // segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
            // return finishProcessingFn(null, segment);
        ...
    }
  
    haveMetadata = function (xhr, url) {
        ...
            // any in-flight request is now finished
            request = null;
            loader.state = 'HAVE_METADATA';
            
            // 解析m3u8正确格式 - 开始
            if (typeof (req.responseText) == "string") {
                /**
                 * 解密m3u8得到的值
                 */
                var _responseText = 解密后的内容;
            } else {
                var _responseText = req.responseText;
            }
            // 解析m3u8正确格式 - 结束

            parser = new _m3u8Parser2['default'].Parser();
            parser.push(_responseText);
            parser.end();
        ...
    }
  
    loader.start = function () {
        ...
            if (error) {
                loader.error = {
                    status: req.status,
                    message: 'HLS playlist request error at URL: ' + srcUrl,
                    responseText: req.responseText,
                    // MEDIA_ERR_NETWORK
                    code: 2
                };
                if (loader.state === 'HAVE_NOTHING') {
                    loader.started = false;
                }
                return loader.trigger('error');
            }


            // 解析m3u8正确格式 - 开始
            if (typeof (req.responseText) == "string") {
                /**
                 * 解密m3u8得到的值
                 */
                var _responseText = 解密后的内容;
            } else {
                var _responseText = req.responseText;
            }
            // 解析m3u8正确格式 - 结束
            
            parser = new _m3u8Parser2['default'].Parser();
            parser.push(_responseText);
            parser.end();
        ...
    }
  
  • 方式二:如果感觉第一种麻烦可以让后端对key进行特殊二次加密(当然前提是前端知道后端是如何加密的以方便如何解密)

  • 方式二解密:

  
  // video.js 7.15.0 (这个版本自带hls功能)
  
    var handleKeyResponse = function handleKeyResponse(segment, objects, finishProcessingFn) {
        return function (error, request) {
            var response = request.response;
            var errorObj = handleErrors(error, request);

            if (errorObj) {
                return finishProcessingFn(errorObj, segment);
            }

            /**
             * 
             * 1、咔咔咔写自己拿到key返回内容进行解密
             * 2、最后得到的是一个16位字符串
             * 3、必须将字符串转换为ArrayBuffer
             * 注:假如某个节视频key解密后为 2y7d9iwu8rc3fa0x
             */

            //字符串转字符串ArrayBuffer
            function str2ab(s,f) {
                var b = new Blob([s],{type:'text/plain'});
                var r = new FileReader();
                r.readAsArrayBuffer(b);
                r.onload = function (){if(f)f.call(null,r.result)}
            }

            // 此处是为了演示,实际应该填写每次解密后的字符串变量
            str2ab('2y7d9iwu8rc3fa0x',function(ab){
                //ab为ArrayBuffer
                var view = new DataView(ab);

                var bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);

                console.log(bytes);

                for (var i = 0; i < objects.length; i++) {
                    objects[i].bytes = bytes;
                }

                return finishProcessingFn(null, segment);
            });


            // 将原来的方法注释掉
            // var view = new DataView(new Int8Array(hexAesStr('516aa4d465a4085baddcac890cbcbf7e')).buffer);
            // var bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);

            // for (var i = 0; i < objects.length; i++) {
            //     objects[i].bytes = bytes;
            // }

            // return finishProcessingFn(null, segment);
        };
    };

3 最后

  • 两种方式都是经过web端测试可行的,以此作为记录,欢迎各位大佬指教