基于HLS的多媒体防盗方案调研
基于HLS的多媒体防盗方案调研
为什么要加密视频
视频加密是为了让要保护的视频不能轻易被下载,即使下载到了也是加密后的内容,其它人解开加密后的内容需要付出非常大的代价。即便如此,也无法严格保护视频不被录制。
常见的防盗技术
- 防盗链:只能合法的通过系统认证的用户才能访问到资源,其实就是资源访问鉴权。
- 加密视频:通过对称加密算法加密视频内容,合法用户获取到解密视频的**,并获取到解密的视频内容,在客户端解密播放。
播放方案
根据经验播放mp3、mp4文件的时候,H5 video或audio播放器通过设置header中的content-range向流媒体服务部分获取一段媒体数据,播放和seek一样,每次获取数据字节数可能比较大,视网络状况不同,等待的时间不同,字节越大缓冲等待的时间越久。参考大型视频网站的做法,发现他们的是将视频文件切片播放的。也就是将大的视频文件分成N小段,比如按Apple推荐标准每10s切成一段。这样的优势是打开视频加载速度快,另外播放第n段的时候,播放器会下载n+1段,n+2段不会下载,大大缓解服务器和带宽的压力。
因此为了提高播放性能,并且防止资源被盗,我们将音视频进行切片,并生成**,用**对音视频数据进行加密。m3u8支持分片,并建立m3u8格式的索引,可用于直播或点播场景。
因此我们采用的ts切片,浏览器播放的流媒体传输协议是HLS。
- HLS:Apple 推出的基于 HTTP 协议的 MP4 分片传输协议,可用于点播和直播场景。每下载一个分片都需要发生一次 HTTP 请求,所以严格来说 HLS 不能称为流媒体传输协议。
流媒体加密原理
流媒体传输协议是把音视频流拆分成连续的小块之后再传输。流媒体加密技术的关键在于,对每一分配采用对称加密算法。服务端加密,通过验证的用户才能使用播放器进行解密。解密的过程是通过m3u8播放列表提供的参数,获取到**key,通key进行解密播放。现有常见的加密技术分为对称加密和非对称加密,对称加解密计算较快,效率较高,适用于流媒体对延时有要求的场景。HLS提供的对称加密算法有AES-128等。
我们的方案
我们的目标是用户只能在我们系统观看视频,但是不能下载(盗用)视频。前文提到为了提高播放性能,我们采用了切片;为了基本的视频安全,我们利用HLS支持的AES-128对视频进行加密。这样做还是不够的,因为播放器需要通过m3u8播放列表提供的参数向服务器发起http请求获取到**,这样通过抓包或者浏览器 F12 network可以看到请求URL和响应体。也就是说能观看的用户可以拿到我们加密的key和视频切片,通过FFmpeg或相关技术应该是可以将切片合成完整视频的。因此这样做还不够安全。我们的方案是将**key进行二次加密,播放器拿到key以后需要先解密一次得到实际的加***,然后再将**给到播放器播放。这样即便拿到接口响应的key也不能拿来解密或用于一般播放器播放。
方案及如何保护**
我们的场景是在Web前端也就是浏览器播放,必须借助浏览器生态支持的技术,并且没有开发我们自己的播放器,有的厂商是有生成自己的视频格式的,需要厂商自己的播放器才能打得开,这样相比会更安全。但是我们目前只能利用hls.js或者video.js来播放HLS。video.js 使用的是videojs-contrib-hls插件,据说videojs-contrib-hls使用的hls.js,其实最终使用的还是hls.js。
video.js
video.js 提供了 XHR 拦截器,对获取key的URI进行替换,这样的好处是说,系统外用户拿到m3u8不能直接用其他播放器打开,但是合法用户还是能拦截或查看到替换后访问的URI。假如m3u8播放列表类似这样:
处理URI的代码片段如下:
<link media="all" rel="stylesheet" href="https://unpkg.com/[email protected]/dist/video-js.css"> <script src="https://unpkg.com/[email protected]/dist/video.js"></script> <video-js id="player"> <source src="//video/index.m3u8" type="application/x-mpegURL" /> </video-js> <script> var player = videojs("player"); var keyPrefix = "key://"; var urlTpl = "https://domain.com/path/{key}"; // player.ready player.on("loadstart", function (e) { player.tech().hls.xhr.beforeRequest = function(options) { // required for detecting only the key requests if (!options.uri.startsWith(keyPrefix)) { return; } options.headers = options.headers || {}; optopns.headers["Custom-Header"] = "value"; options.uri = urlTpl.replace("{key}", options.uri.substring(keyPrefix.length)); }; }); </script>
显然这个方法不能起到理想的防盗效果。
hls.js
HLS只允许将**与播放器可以检索的内容链接起来,但它没有指定如何保护**的特定方法。如果需要对内容和**执行访问控制,我们可以像处理其他想保护的资源一样,因为播放器通过HTTP链接访问它。例如,可以使用某种登录令牌来保护**,该令牌将在请求头中传递。不幸的是,hls.js还不支持为请求key配置客户化请求头。这里通常的做法是在配置中传递一个loader,基于现有的loader实现自己的加载器,然后将自定义的HTTP头放入其中,或者对返回的**数据做处理(比如解密)。另外hls.js自定义分片请求的URL,可以参考这篇文章。
我们在hls.js的配置项李找到了对loader的介绍:
(default: standard
XMLHttpRequest
-based URL loader)Override standard URL loader by a custom one. Use composition and wrap internal implementation which could be exported by
Hls.DefaultConfig.loader
. Could be useful for P2P or stubbing (testing).Use this, if you want to overwrite both the fragment and the playlist loader.
Note: If
fLoader
orpLoader
are used, they overwriteloader
!
我们试着通过改写这个loader方法来满足我们的业务场景:
var configure = { //MEU8加载器 loader : function() { const loader = new Hls.DefaultConfig.loader(configure); this.abort = () => loader.abort(); this.destroy = () => loader.destroy(); this.load = (context, config, callbacks) => { const { type } = context; const onSuccess = callbacks.onSuccess; callbacks.onSuccess = (response, stats, context1, networkDetails) => { if (type !== "manifest" && context1.url.endsWith(".key")) { console.log(context1.url) // 这里对返回的**数据进行处理 // response.data = dealWithKeyData(response.data)); } onSuccess(response, stats, context, networkDetails); }; loader.load(context, config, callbacks); }; } }; if(Hls.isSupported()) { var hls = new Hls(configure); hls.loadSource(videoSrcInHls); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED,function() { video.play(); }); } else { addSourceToVideo(video, videoSrcInMp4, 'video/mp4'); video.play(); }
**的其他保护
m3u8的播放列表以上面截图为例,请注意#EXT-X-KEY这个参数提供了加***的URI。播放器将从该位置检索**以解密媒体段。为了防止**被窃听,应该通过HTTPS对其进行服务。可能还需要实现某些身份验证机制,以限制谁可以访问**。在这种情况下,所有段都使用相同的**加密。如果暴露特定**,则定期更改加***以最小化影响可能是有益的,这被称为**旋转。针对动态更换**这点,需要后端能力支撑。设想生成**以后需要对切片进行加密,更换一次**需要等待较长时间,不一定能满足播放实时性要求。
参考文章
https://onetdev.medium.com/custom-key-acquisition-for-encrypted-hls-in-videojs-59e495f78e52
http://hlsbook.net/how-to-encrypt-hls-video-with-ffmpeg/
https://github.com/videojs/videojs-contrib-hls/issues/1337
https://doc.xuwenliang.com/docs/video_audio/3422
https://imweb.io/topic/59819d7bf8b6c96352a593ff
https://github.com/video-dev/hls.js/issues/1437
https://zhuanlan.zhihu.com/p/102125509