zoukankan      html  css  js  c++  java
  • 大文件上传和断点续传

    简要思路

    为什么大文件上传需要做特殊处理呢?文件的上传,是通过一个 POST 提交表单,把文件的二进制数据写在请求里面来发送的。对于主流的 Web 服务器,一般都会有一个请求体 body 的大小限制,比如限制在 2MB 以内,那么一个超过 2MB 的文件就无法塞进一个请求了,需要对这个文件进行分片,通过多个 POST 来传输。那是否可以通过调整 body 的大小来适应大文件呢?其实是可以的,不过会有缺点。链接 [1, 2, 3] 指出,如果允许请求体过大,可以针对接口进行慢速 POST DOS 攻击,即控制很多主机向服务器慢速发送请求体,长时间占用一个连接,消耗服务器的资源(Tomcat 有最大的 socket 连接数,BIO 默认有 200,NIO 默认有 10000)。不过链接 [4] 指出,慢速 POST DOS 攻击并不是一种最有效的攻击方式,人们有大把别的更好的攻击方式,如果要进行 DOS 攻击,那为什么不选择别的方式呢。这启发我们,在考虑系统安全性的时候,要考虑攻击者是否有比这更好的攻击方法,如果有,那么存在这些缺陷也是可以容忍的。

    总结一下,大文件上传,可行的策略是分片上传,当然调整 NGINX 的 client_max_body_size 完全也可行,即使确实存在慢速 POST DOS 攻击这种缺陷。本文采用的方式是分片上传。为什么采用分片上传呢?因为分片上传有额外的好处,断点续传,多线程上传。断点续传,不管是上传还是下载,原理都是一样的,通过随机写入文件来实现。比如多线程下载文件,我们可以先开一块和文件大小相同的文件,接着使用不同线程下载文件不同部分,写入到对应部分即可。在 Java 中使用 RandomAccessFile 来实现随机写入。断点上传的续传应该如何实现呢?首先在浏览器端,我们可以使用 JavaScript 文件相关的 API slice,对文件进行分块,之后分块上传,调用对应的 API 进行分块上传。如果用户刷新了网页,或者关掉了浏览器,传输中断了,下次传输的时候,通过调用接口获取缺失的文件分块号,根据获取到的文件分块号选择对应的块进行上传。

    这里有一个问题需要解决,如何知道用户上传的是同一个文件呢?在浏览器端,因为安全策略,是不能获取本地路径来唯一标识这个文件的。如果对整个文件计算散列值,一方面内存可能会爆,比如读入一个 4G 的文件来计算散列,另一方面速度很慢,链接 [5] 文末有不同大小文件的数据,1G 的文件大概需要 30 秒,这可能影响了用户体验。我设计的几个接口,可以保证文件的完整性,所以完全没有必要计算整个文件计算散列值,取前 5 ~ 10 MB 计算散列作为唯一标识。

    后端实现

    后端实现了四个接口。

    • 尝试获取文件对应的 id 号,对应的元组存储了文件的路径。根据散列,尝试获取 id 号,根据具体任务再加上一些其他信息共同组成唯一标识。
    • 上传大文件,任务初始化。在本地创建一个同等大小的文件。接口需要,散列值,文件名,文件大小的信息,以及其他非文件相关的信息。
    • 上传大文件的一个分块。使用 RandomAccessFile 写入到对应的位置。
    • 获取缺失的文件块号。使用 Redis 来存储已经接收的文件块号,设置超时时间为四个小时,每次接收文件块都会重置计时。

    此外还是用了 JWT 来进行用户鉴权,避免每次接口请求都走数据库鉴权的那个方法。文件的完整性有 TCP 和逻辑来保证,TCP 可以保证每一块是正确的,而我的逻辑可以保证整个文件每一块都有,因此整个文件也就是完整的了。

    前端实现

    使用 axios 来实现接口调用,使用 crypto 计算散列值。

    链接 [6] 给出了计算文件散列的方法,链接 [7] 是 crypto 关于散列值计算的接口,具体实现的时候使用的摘要算法是 SHA-256。

    因为整个逻辑流程有地方不可以异步,很多操作依赖前面的结果。对于那些操作,可以采用 async 异步方法,搭配 await 关键词,等待异步运行结束。

    计算散列的方法,并且将结果转为十六进制的字符串,下面的代码结合了 [6, 7] 两个链接的内容,BLOCK_SIZE 设置为 1MB。

    let firstMegaBlock = file.slice(0, 10 * that.BLOCK_SIZE)
    var firstMegaBlob = new FileReader()
    firstMegaBlob.readAsArrayBuffer(firstMegaBlock)
    firstMegaBlob.onloadend = async function() {
        let hashArrayBuffer = await crypto.subtle.digest("SHA-256", firstMegaBlob.result)
        const hashArray = Array.from(new Uint8Array(hashArrayBuffer))
        const hashStr = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
        console.log(hashStr)
        hash = hashStr
    }
    

    参考链接

    1. https://www.cnblogs.com/yjf512/archive/2013/03/29/2988296.html#:~:text=在这种情境下,你的业务中的所有POST请求都是不安全的!!只要进行DDOS攻击,业务就会瘫痪。
    2. https://stackoverflow.com/questions/51069155/maximum-recommended-client-max-body-size-value-on-nginx#:~:text=large body upload attack
    3. https://zhuanlan.zhihu.com/p/30635804#:~:text=攻击的影响。-,3.2.3 慢速POST请求攻击,-慢速POST请求****
    4. https://security.stackexchange.com/questions/182696/what-are-the-potential-vulnerabilities-of-allowing-a-large-http-body-size
    5. https://medium.com/@0xVaccaro/hashing-big-file-with-filereader-js-e0a5c898fc98
    6. https://stackoverflow.com/questions/20623467/calculate-the-hash-of-blob-using-javascript
    7. https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
  • 相关阅读:
    springboot-项目属性配置
    网络编程
    HttpServlet---getLastModified与缓存
    TCP的三次握手和四次挥手
    数据库事务隔离级别-- 脏读、幻读、不可重复读
    js 数组,字符串,json互相转换
    表格单元格td设置宽度无效的解决办法
    判断IE版本的HTML语句详解,如:<!--[if IE 9]> 仅IE9可识别 <![endif]-->
    IE主页被篡改为360导航终极处理办法
    前端常见问题
  • 原文地址:https://www.cnblogs.com/zzk0/p/14956568.html
Copyright © 2011-2022 走看看