使用 Cipher CipherInputStream CipherOutputStream 实现对文件的加解密
每个文件使用一个秘钥 String aesKey = UUID.randomUUID().toString().replace("-","");
可以通过uuid or 其他的途径生成一个唯一的秘钥。
private static final String ALGORITHM_STREAM = "AES/ECB/PKCS5Padding";
/**
* 加密数据
*
* @param input
* @param key
* @return
* @throws Exception
*/
public static InputStream encodeStream(InputStream input, String key) throws Exception {
SecretKey secretKey = generateAesKey(key);
Cipher c = Cipher.getInstance(ALGORITHM_STREAM);
c.init(1, secretKey);
return new CipherInputStream(input, c);
}
/**
* 解密文件流信息
*
* @param input
* @param key
* @return
* @throws Exception
*/
public static InputStream decodeStream(InputStream input, String key) throws Exception {
SecretKey secretKey = generateAesKey(key);
Cipher c = Cipher.getInstance(ALGORITHM_STREAM);
c.init(2, secretKey);
return new CipherInputStream(input, c);
}
private static SecretKey generateAesKey(String key) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
return keyGenerator.generateKey();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
样例eg: http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4
当前这个视频播放实现随机播放、实现分块下载等等能力,一般情况下后端下载视频
http://localhost:8080/web/file-upload/common-file-download?fileId=ab87ef175dc1419b922acb35dd3ad58e
提供类似的URL地址 后端直接写流到浏览器 IOUtils.copy(encodeInputStream, response.getOutputStream());
当点击视频中进度条的时候永远都不行,点击进度条相当于重新请求、视频流信息重0开始,直接破坏了进度条的规则。
查询了一些资料,其实视频播放的时候通过Range 进行了分段的请求、我们可以对于流量进行分段的下载处理
206 状态码 & Content-Range 分段的响应下载,读取当前流信息中指定开始到指定长度的流信息
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
@GetMapping(value = "/common-file-download")
@ResponseBody
public void commonFileDownload(@RequestParam String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception {
File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
//获取从那个字节开始读取文件
try (FileInputStream inputStream = new FileInputStream(desc)) {
InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
int fSize = FILE_LENGTH;
if (this.haveRanges(request)) {
// 断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
//开始下载位置
int startByte = rangeInfo.getRight();
//结束下载位置
int endByte = rangeInfo.getLeft();
//要下载的长度
int contentLength = endByte - startByte + 1;
//Content-Length 表示资源内容长度,即:文件大小
response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
//Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
return;
}
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copy(encodeInputStream, response.getOutputStream());
} catch (ClientAbortException e) {
log.debug("ignore {}", e.getMessage());
} catch (Exception e) {
log.error("error", e);
} finally {
IOUtils.close(response.getOutputStream());
}
}
/**
* 获取 range 的长度信息
*
* @param range
* @param fileSize
* @param defaultRangeLengthSize
* @return
*/
public MutablePair<Integer, Integer> getRangeInfo(String range, int fileSize, int defaultRangeLengthSize) {
MutablePair<Integer, Integer> rangeInfo = new MutablePair<>();
rangeInfo.setLeft(fileSize - 1);
rangeInfo.setRight(0);
if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) {
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split("-");
int startByte = 0;
int endByte = fileSize - 1;
try {
//根据range解析下载分片的位置区间
if (ranges.length == 1) {
//情况1,如:bytes=-1024 从开始字节到第1024个字节的数据
if (range.startsWith("-")) {
endByte = Integer.parseInt(ranges[0]);
}
//情况2,如:bytes=1024- 第1024个字节到最后字节的数据
else if (range.endsWith("-")) {
startByte = Integer.parseInt(ranges[0]);
//增加一个默认的信息
endByte = startByte + defaultRangeLengthSize;
if (endByte >= fileSize - 1) {
endByte = fileSize - 1;
}
}
}
//情况3,如:bytes=1024-2048 第1024个字节到2048个字节的数据
else if (ranges.length == 2) {
startByte = Integer.parseInt(ranges[0]);
endByte = Integer.parseInt(ranges[1]);
}
} catch (NumberFormatException e) {
startByte = 0;
endByte = fileSize - 1;
}
rangeInfo.setRight(startByte);
rangeInfo.setLeft(endByte);
}
return rangeInfo;
}
/**
* 是否有Range
*
* @param request
* @return
*/
public boolean haveRanges(HttpServletRequest request) {
String range = request.getHeader(HttpHeaders.RANGE);
if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) {
return true;
}
return false;
}
感觉一切都很好了… 本地文件也可以了…
下载非常的慢
http://localhost:8080/web/file-upload/version1-file-download
为什么?主要是下面这个代码每次都需要下载跳过的流文件进行解密,随着长度的加深 速度越来越慢
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray())
/**
* 下载比较慢
*
* @param fileId
* @param request
* @param response
* @throws Exception
*/
@GetMapping(value = "/version1-file-download")
@ResponseBody
public void version1(@RequestParam(required = false) String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception {
File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
fileId = "Qq87r0SidfdM9i5QDrtKLTbRpcGam0qd";
S3Object object = s3.getObject(BUCK_NAME, fileId);
try (InputStream inputStream = object.getObjectContent()) {
InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
int fSize = FILE_LENGTH;
if (this.haveRanges(request)) {
// 断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
//开始下载位置
int startByte = rangeInfo.getRight();
//结束下载位置
int endByte = rangeInfo.getLeft();
//要下载的长度
int contentLength = endByte - startByte + 1;
//Content-Length 表示资源内容长度,即:文件大小
response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
//Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
return;
}
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copy(encodeInputStream, response.getOutputStream());
} catch (ClientAbortException e) {
log.debug("ignore {}", e.getMessage());
} catch (Exception e) {
log.error("error", e);
} finally {
IOUtils.close(response.getOutputStream());
}
}
http://localhost:8080/web/file-upload/version2-file-download
为了解决下载响应非常慢的问题,需要通过s3 支持的分块下载
支持通过header 进行分块下载…
GetObjectRequest getObjectRequest = new GetObjectRequest(BUCK_NAME, fileId);
int startOffset = rangeInfo.getRight();
getObjectRequest.setRange(startOffset);
object = s3.getObject(getObjectRequest);
感觉好像好了,其实还有问题,任意拖住进度条一会就不能播放了…自然就停止了…
看到报错,不过这个问题很关键 下载的内容长度没有16的整数倍
java.io.IOException: javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:128)
at java.base/javax.crypto.CipherInputStream.read(CipherInputStream.java:242)
at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1384)
at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1342)
**
* 下载偶尔失败
*
* @param fileId
* @param request
* @param response
* @throws Exception
*/
@GetMapping(value = "/version2-file-download")
@ResponseBody
public void version2(@RequestParam(required = false) String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception {
File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
fileId = "Qq87r0SidfdM9i5QDrtKLTbRpcGam0qd";
S3Object object = null;
boolean isRange3s = false;
if (haveRanges(request)) {
GetObjectRequest getObjectRequest = new GetObjectRequest(BUCK_NAME, fileId);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), FILE_LENGTH, 5 * 1024 * 1024);
int startOffset = rangeInfo.getRight();
getObjectRequest.setRange(startOffset);
object = s3.getObject(getObjectRequest);
isRange3s = true;
} else {
object = s3.getObject(BUCK_NAME, fileId);
}
try (InputStream inputStream = object.getObjectContent()) {
InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
int fSize = FILE_LENGTH;
if (this.haveRanges(request)) {
// 断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
//开始下载位置
int startByte = rangeInfo.getRight();
//结束下载位置
int endByte = rangeInfo.getLeft();
//要下载的长度
int contentLength = endByte - startByte + 1;
//Content-Length 表示资源内容长度,即:文件大小
response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
//Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
if (isRange3s) {
// Not all bytes were read from the S3ObjectInputStream, aborting HTTP connection. This is likely an error and may result in sub-optimal behavior. Request only the bytes you need via a ranged GET or drain the input stream after use.
// 忽略这个错误..
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), 0, contentLength, IOUtils.byteArray());
} else {
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
}
return;
}
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copy(encodeInputStream, response.getOutputStream());
} catch (ClientAbortException e) {
log.debug("ignore {}", e.getMessage());
} catch (Exception e) {
log.error("error", e);
} finally {
IOUtils.close(response.getOutputStream());
}
}
为什么第二个版本有问题?
AES wikipedia
AES加密过程是在一个4×4的字节矩阵上运作,比如【1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16】16个字节一起加密,没有16个字节进行填充处理。
AES 加密后的大小=(AES 加密前的大小/16+1)*16 按照这样的说法、解密的时候只能 1~16一起解密,不能 2~17一起解密。因此要计算当前 startRange 最小的16块的起始位置作为开始点
进行Range 下载 startOffset =(startByte / 16) * 16
,这样下载后其实文件流多读取了一部分,所以在响应的时候要跳过这个部分
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte - startOffset, contentLength, IOUtils.byteArray());
整体上就完美的解密、完美定位到具体的字节流的信息了。
http://localhost:8080/web/file-upload/version3-file-download
/**
* 正常版本
*
* @param fileId
* @param request
* @param response
* @throws Exception
*/
@GetMapping(value = "/version3-file-download")
@ResponseBody
public void version3(@RequestParam(required = false) String fileId, HttpServletRequest request, HttpServletResponse response) throws Exception {
File desc = new File(System.getProperty("user.dir") + "/file/" + fileId);
fileId = "Qq87r0SidfdM9i5QDrtKLTbRpcGam0qd";
S3Object object = null;
boolean isRange3s = false;
if (haveRanges(request)) {
GetObjectRequest getObjectRequest = new GetObjectRequest(BUCK_NAME, fileId);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), FILE_LENGTH, 5 * 1024 * 1024);
int startOffset = rangeInfo.getRight();
startOffset = (startOffset / 16) * 16;
getObjectRequest.setRange(startOffset);
object = s3.getObject(getObjectRequest);
isRange3s = true;
} else {
object = s3.getObject("owork-file-demo", fileId);
}
try (InputStream inputStream = object.getObjectContent()) {
InputStream encodeInputStream = AesUtils.decodeStream(inputStream, fileId);
int fSize = FILE_LENGTH;
if (this.haveRanges(request)) {
// 断点续传
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
MutablePair<Integer, Integer> rangeInfo = this.getRangeInfo(request.getHeader(HttpHeaders.RANGE), fSize, 5 * 1024 * 1024);
//开始下载位置
int startByte = rangeInfo.getRight();
//结束下载位置
int endByte = rangeInfo.getLeft();
//要下载的长度
int contentLength = endByte - startByte + 1;
int startOffset = (startByte / 16) * 16;
//Content-Length 表示资源内容长度,即:文件大小
response.setHeader(HttpHeaders.CONTENT_LENGTH, contentLength + "");
//Content-Range 表示响应了多少数据,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + "-" + endByte + "/" + fSize);
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + fileName);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
if (isRange3s) {
// Not all bytes were read from the S3ObjectInputStream, aborting HTTP connection. This is likely an error and may result in sub-optimal behavior. Request only the bytes you need via a ranged GET or drain the input stream after use.
// 忽略这个错误..
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte - startOffset, contentLength, IOUtils.byteArray());
} else {
IOUtils.copyLarge(encodeInputStream, response.getOutputStream(), startByte, contentLength, IOUtils.byteArray());
}
return;
}
String fileName = URLEncoder.encode("test.mp4", StandardCharsets.UTF_8);
String mimetype = Mimetypes.getInstance().getMimetype(fileName);
response.setContentType(mimetype);
IOUtils.copy(encodeInputStream, response.getOutputStream());
} catch (ClientAbortException e) {
log.debug("ignore {}", e.getMessage());
} catch (Exception e) {
log.error("error", e);
} finally {
IOUtils.close(response.getOutputStream());
}
}
从文件加密到视频分段视频分段渐进式播放问题一路探究,了解了文件的分片下载的实现原理,对于加密文件的处理如果实现分片下载失败原理进行探究。