H5视频流加密


之前做过在页面上实现音频播放功能,但是业务又提出一个需求,就是音频只能在线播放,不允许下载。经过一番研究发现前端页面屏蔽下载功能只是个障眼法,用户可以直接请求后端接口下载得到原音频文件,因此要实现禁止下载功能还是得在后端接口上做文章,一开始的思路,在下载接口上加一些同源,token认证之类的权限校验,不满足条件则不允许下载。不过后来又觉得不行,因为这些参数都是可以模拟的,如果这些用户模拟这些参数向接口发送请求,一样可以得到原文件。于是又想到了另一种方案,即后端接口对原文件加密,输出加密后的文件,前端播放时再解密。这样即使直接请求后端接口得到的也是加密后的文件,无法播放。

在此做一下记录,前端有两种方案,第一种是通过BlobURL访问资源,对于大文件,这种方式性能上会好很多。第二种是使用DataURL将源文件转为base64,这种方式看不到访问路径,前端也无法下载,可以满足需求,但是对于大文件不建议使用这种方式

Blob URL方式

Blob URL是blob协议得URL,它的格式是:blob:http://xxx。Blob URL可以通过URL.createObjectURL(blob)创建。在绝大部分场景下,我们可以像使用Http协议得URL一样使用Blob URL。这种方式要把整个文件先下载下来,然后创建URL,对于非常大的文件例如视频流,其实也不建议使用。

Java接口代码

   @GetMapping("/audio/player")
    public void player(
                         HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        File file = new File("F:\\test\\十年.mp3");
        if(!file.exists())
            throw new RuntimeException("音频文件不存在 --> 404");
        String range = request.getHeader("Range");
        if(range == null)
            range = "bytes=0-";
        String[] rs = range.split("=");
        range = rs[1].split("-")[0];
        long length = file.length();
        int irange = Integer.parseInt(range);
        length = length - irange;

        response.addHeader("Accept-Ranges", "bytes");
        response.addHeader("Content-Length", (length) + "");
        response.addHeader("Content-Range", "bytes " + range + "-" + (length) + "/" + (length));
        response.addHeader("Content-Type", "audio/mpeg;charset=UTF-8");

        OutputStream os = response.getOutputStream();
        byte[] b = new byte[1024];
        int len ;
        FileInputStream fis = new FileInputStream(file);
        // 在原文件中,每1kb前面插入1个字节长度的字符 '-'
        byte[] fs = new byte[]{'-'};
        while ((len = fis.read(b)) != -1) {
            // 插入字符
            os.write(fs, 0, 1);
            os.write(b, 0, len);
        }
        fis.close();
        os.close();
    }

前端H5代码

           <audio id="my-audio" :src="audioUrl" controls="controls" preload="auto"></audio>

           //解码播放
            fetch('/audio/player').then(res => res.blob().then(blob => {
                    console.log('请求接口结束');
                    let start =1;
                    let blobs = [];
                    console.log('开始解码');
                    while (true){
                        let end = start + 1024;
                        let slip = blob.slice(start,end);
                        if(slip.size === 0){
                            break;
                        }
                        blobs.push(slip);
                        start = end+1;
                    }
                    // 合并
                    let mergeBlob = new Blob(blobs,{type:"audio/mpeg"});
                    let audioUrl = URL.createObjectURL(mergeBlob);
                    console.log('密码:'+audioUrl.password);
                    console.log('字符串url:'+audioUrl.toString());
                    // 使用了vue,这里的this指的是vue实例
                    this.audioUrl = audioUrl;

                })
            );

            // 加载完毕后释放资源
            let myAudio = document.getElementById('my-audio');
            myAudio.oncanplay = function() {
                console.log('audio加载完成');
                window.URL.revokeObjectURL(this.audioUrl);
                console.log('audio清除完成');
            }

BlobURL总结

经过后端加密,前端再解密播放后,打开页面时,可以正常播放流,但是直接调用后端接口时,已不能正常播放。下图是页面正常加载了解析过的流

直接调用后端接口,如下

可以看到本来是音频文件的流,打开之前显示的是视频,且无法播放。说明后端原文件加密成功了。

存在的问题

这种方式确实做到了后端加密,直接请求接口无法得到原文件,但是前端播放后仍然能得到解密后的文件。因为前端是通过blob创建url访问的,F12后可以看到url,直接请求这个url仍然能得到解码后的源文件

因此刚刚加密的方式只是后端实现了加密,对于前端来说,只要播放了还是有办法拿到原文件。原因在H5的媒体标签如<audio><video> 标签的src属性只接收一个原文件的地址。也就是说src的地址指向的一定是一个正常播放的原文件,而不是一个加密文件。因此本质上H5不能实现原文件不允许下载的功能。利用自定义的控件如flash可以实现加密播放,不过flash已经被谷歌淘汰了,谷歌浏览器马上要不支持flash控件了。

Data URL方式

有使用过base64来预览图片经验的,对这个应该并不陌生,Web性能优化有一项措施:把小图片用base64编码直接嵌入到HTML文件中,实际就是利用了Data URL来获取图片数据。由于使用Blob URL方式浏览器F12还是能看到blob的下载连接,还是能得到原文件,于是便想到通过base64对音频文件加密,这样前端就只能看到base64文本了,无法直接得到原文件,后端代码不变,前端调整如下

前端H5代码

          <audio id="my-audio" :src="audioUrl" controls="controls" preload="auto"></audio>

             // 解码播放  Data URL
            fetch('/audio/player').then(res => res.blob().then(blob => {
                    console.log('请求接口结束');
                    let start =1;
                    let blobs = [];
                    console.log('开始解码');
                    while (true){
                        let end = start + 1024;
                        let slip = blob.slice(start,end);
                        if(slip.size === 0){
                            break;
                        }
                        blobs.push(slip);
                        start = end+1;
                    }
                    // 合并
                    let mergeBlob = new Blob(blobs,{type:"audio/mpeg"});

                    let fileReader = new FileReader();
                    // 使用了vue,这里的this指的是vue实例
                    let self = this;
                    fileReader.onload = function(e) {
                        self.audioUrl = e.target.result;
                        console.log('base:'+self.audioUrl);
                    };
                    fileReader.readAsDataURL(mergeBlob);


                })
            );

BlobURL总结

音频转base64就完成了,这样做有一个弊端对于大文件会导致网页卡顿,我经过测试一个48MB的音频使用base64加密,网页就大约卡了10多秒。这对于一般的C端网站都是难以接受的,我的需求中音频文件是通话录音,所以基本上不会很大,于是决定采用这种方式。主要还是业务禁止下载的需求给逼的o(╥﹏╥)o 下面来看一下页面效果吧

可以看到audio标签的src属性变成了一个base64的字符串,拿到base64的字符串,通过算法也可以把base64字符串还原成原文件,不过当我要去复制时,会提示太大无法编辑,这正是我需要的结果,哈哈。

F12中请求也只出现了一个后端请求。base64比较完美的解决了我的需求,唯一不足是对于大文件的编码

Blob URL和Data URL有什么区别

  • blob显示的形式blob:http://xxx,dataURL的显示形式data:image/jpeg;base64,/9j/4AAQ...
  • Blob URL的长度一般比较短,Data URL因为直接存储图片base64编码后的数据,往往很长。浏览器在显示Data URL时使用了省略号(…)。当显式大图片时,使用Blob URL能获取更好的可能性。
  • Blob URL 只能在当前应用内部使用,把Blob URL复制到浏览器的地址栏中,是无法获取数据的。Data URL相比之下,就有很好的移植性,你可以在任意浏览器中使用。

Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
红黑树原理 红黑树原理
说到红黑树,让人又爱又恨啊,真是一种令人头大的数据结构。我也没少被他折磨过,尽管在工作中,在网文中多次看到过关于它的原理解释,但大多都是一上来就是五条定义,然后紧跟着就是红黑树插入的代码实现,翻转代码实现,让人难以理解消化,搞的人云里雾里的
2020-05-11
Next 
H5播放视频流 H5播放视频流
由于项目需求,在公司系统中需要播放录音。前端我就采用了H5的 <audio> 标签,而后端就是常规 的输出流操作,即通过response的outPutStream输出文件,对于前端来说就是文件下载。开始测试的时候因为使用的都是一
2020-05-08
  TOC