之前做过在页面上实现音频播放功能,但是业务又提出一个需求,就是音频只能在线播放,不允许下载。经过一番研究发现前端页面屏蔽下载功能只是个障眼法,用户可以直接请求后端接口下载得到原音频文件,因此要实现禁止下载功能还是得在后端接口上做文章,一开始的思路,在下载接口上加一些同源,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相比之下,就有很好的移植性,你可以在任意浏览器中使用。