nginx delete form表单 收不到参数_HTTP 文件上传的一个后端完善方案(NginX)

(给PHP开发者加星标,提升PHP技能)

转自:林伯格

https://breeze2.github.io/blog/scheme-nginx-php-js-upload-process

前言

很多网站都会有上传文件的功能,比如上传用户头像,上传个人简历等等,除非是网盘类的网站,一般上传文件不会作为网站的主要功能;而且,如今大众的网速已经是足够的快,上传几百KB的文件,几乎可以秒内完成。

但是,随着文件大小和类型越来越庞大,文件上传也就越值得我们重视。

大多数网站,对于上传文件的处理,都是简单的前端POST上传,后端验证存放然后返回访问地址。毕竟,文件小,网速快,一瞬间的事情谁会多在意呢?

存在问题

假设我们有一个网站,基于NginX+PHP+JS构架,网站允许用户上传一些小视频、音乐或者PPT等文件在线上展示,单个文件大小限制不超过30MB,那么我们要怎样实现这个上传功能呢?

限制上传文件的大小

首先,NginX要能接受最大32MB的请求(除了最大文件本身30MB,再预留一些给其他请求参数),我们会修改网站的虚拟主机配置:

# website.conf server {    client_max_body_size 32M;     ...}

然后,PHP也要修改配置,接受最大30MB的文件上传和最大32MB的POST请求:

# php.iniupload_max_filesize = 30M;post_max_size = 32M;

其实,单凭client_max_body_size,NginX是不能真正限制上传文件大小的,因为NginX会先让客户端(一般是浏览器)开始上传请求,直到上传的内容大小超过了限制,NginX才会中止上传,报413 Request Entity Too Large错误,没超过限制则交给PHP处理。

于是,PHP的upload_max_filesizepost_max_size就更没用了,因为PHP获取到文件信息的时候,上传过程已经结束了(这时当然是上传成功,NginX中止请求的话PHP不会进场)。在NginX传递请求结果前,PHP什么(比如验证用户,验证权限等等)都做不了。

如果用户上传了一个大于32MB的时候,直到上传到32MB的时候才能告诉用户文件过大了,那么前面的时间用户不就白等了吗?而且服务器的带宽还是一样被消耗了。

我们更希望在上传开始前就能告诉用户文件过大了。很多网站开发,都会把这一步交给JS处理,在新型浏览器(支持HTML5)里,JS的确可以获取input文件的大小;在旧的IE里,也可以通过ActiveX来实现。但是JS的限制处理很容易被绕过去,只要知道上传地址,一个form标签就能把文件传过去:

<form id="upload_form" action="/path/to/upload" enctype="multipart/form-data" method="post">    <input type="file" name="upload_file" value="/path/of/big/big/file" />    <input type="submit" value="Upload" />form>

正常的用户当然不会这样做,但是有意攻击网站的人会。

限制上传文件的速度

如果服务器的入口带宽是100mbps,用户的上行带宽是10mbps,用户上传一个30MB的文件至少需要30秒,那么在30秒内,服务器的带宽只能满足10个用户上传文件,带宽被占满后,服务器就很难再处理其他请求了。

所以,限制用户上传文件的速度就很有必要。目前,JS做不到限制上传文件的速度,PHP也做不到。

上传文件的进度

用户上传一个30MB的文件至少需要30秒,那么30秒内应该告知用户上传的进度,不能让用户无感知的等待。HTML5改进了XMLHttpRequest对象,在支持HTML5的新型浏览器里,JS可以获取XMLHttpRequest上传文件的进度;在旧的浏览器的也可以通过Flash与JS结合(比如SWFUpload),从而获取上传文件的进度。

但是新型浏览器里,Flash已经被摒弃了,因而要支持新旧浏览器,JS就要写成两套代码。在这里PHP也是帮不上忙,因为PHP拿到传文件信息的时候,上传已经结束了。

解决方案

网站是NginX+PHP+JS构架的,PHP和JS解决不了的问题,那应该在NginX上解决它。NginX虽然是一个现成的软件,但是它还是可以继续扩展和修改的。

NginX本身没有提供上传文件的复杂处理功能,而在NginX官方认可的第三方扩展模块里,有两个模块可以帮助我们实现复杂的上传文件功能,分别是nginx-upload-module和nginx-upload-progress-module。

要将nginx-upload-module和nginx-upload-progress-module编译进NginX,首先要下载NginX源码和nginx-upload-module、nginx-upload-progress-module这两个模块的源码,然后在NginX源码目录中,在configure参数中加入这两个这两个模块,最后make install,大概的执行命令:

$ cd ~$ mkdir tmp$ cd tmp$ wget http://nginx.org/download/nginx-1.11.3.tar.gz$ tar -xvzf nginx-1.11.3.tar.gz$ git clone https://github.com/vkholodkov/nginx-upload-module.git$ git clont https://github.com/masterzen/nginx-upload-progress-module.git$ cd nginx-1.11.3$ ./configure --add-module=~/tmp/nginx-upload-module --add-module~/tmp/nginx-upload-progress-module ...$ make$ make install

如果系统上已经安装过NginX并且所安装NginX版本支持动态模块,那么可以考虑将nginx-upload-module和nginx-upload-progress-module编译成动态模块,这样就不需要重新安装NginX。

nginx-module-libs上有Ubuntu系统上主线NginX版本的一些动态模块,可以上面下载适配你的nginx-upload-module和nginx-upload-progress-module。

下面主要介绍一下两个模块的用法:

nginx-upload-module

当上传文件的体积小于client_max_body_size时, nginx-upload-module可以帮助我们限制上传速度,使用方法见下。

NginX的站点配置:

# website.confserver {    ...    client_max_body_size 32m;    # 限制上传速度最大2Mbps    upload_limit_rate 256k;    location /upload {        # 限制上传文件最大30MB        upload_max_file_size 30m;        # 后续交给 upload.php 处理        upload_pass /upload.php;        # 指定上传文件存放目录,1表示按1位散列,将上传文件随机存到指定目录下的0、1、2、...、8、9目录中(这些目录要手动建立)        upload_store /tmp 1;        # 上传文件的访问权限,user:r表示用户只读        upload_store_access user:r;        # 设置请求体的字段        upload_set_form_field "${upload_field_name}_name" "$upload_file_name";        upload_set_form_field "${upload_field_name}_content_type" "$upload_content_type";        upload_set_form_field "${upload_field_name}_path" "$upload_tmp_path";        # 指示后端关于上传文件的md5值和文件大小        upload_aggregate_form_field "${upload_field_name}_md5" "$upload_file_md5";        upload_aggregate_form_field "${upload_field_name}_size" "$upload_file_size";        upload_pass_form_field "^submit$|^description$";        # 若出现如下错误码则删除上传的文件        upload_cleanup 400 404 499 500-505;    }}

上传文件的页面:

<form id="upload" enctype="multipart/form-data" action="/upload" method="post" >    <input name="upload_file" type="file" label="fileupload" />    <input type="submit" value="Upload File" />form>

处理上传结果的脚本:

 php// upload.phpprint_r($_REQUEST);

如果对PHP解析使用了优雅链接,比如Laravel,那么应该这样使用:

NginX的站点配置:

# website.confserver {    ...    client_max_body_size 32m;    # 限制上传速度最大2Mbps    upload_limit_rate 256k;    location / {        try_files $uri $uri/ /index.php?$query_string;    }    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_param HTTP_PROXY "";        fastcgi_pass unix:/run/php/php-fpm.sock;    }    location @upload_handle {        rewrite ^ /index.php last;    }    location /upload {        # 限制上传文件最大30MB        upload_max_file_size 30m;        # 后续交给 index.php 处理        upload_pass @upload_handle;        # 指定上传文件存放目录,1表示按1位散列,将上传文件随机存到指定目录下的0、1、2、...、8、9目录中(这些目录要手动建立)        upload_store /tmp 1;        # 上传文件的访问权限,user:r表示用户只读        upload_store_access user:r;        # 设置请求体的字段        upload_set_form_field "${upload_field_name}_name" "$upload_file_name";        upload_set_form_field "${upload_field_name}_content_type" "$upload_content_type";        upload_set_form_field "${upload_field_name}_path" "$upload_tmp_path";        # 指示后端关于上传文件的md5值和文件大小        upload_aggregate_form_field "${upload_field_name}_md5" "$upload_file_md5";        upload_aggregate_form_field "${upload_field_name}_size" "$upload_file_size";        upload_pass_form_field "^submit$|^description$";        # 若出现如下错误码则删除上传的文件        upload_cleanup 400 404 499 500-505;    }}

上传文件的页面:

<form id="upload?_token={{csrf_token()}}" enctype="multipart/form-data" action="/upload" method="post" >    <input name="upload_file" type="file" label="fileupload" />    <input type="submit" value="Upload File" />form>

Laravel路由配置:

<?php // routes/web.phpRoute::post('/upload', 'Web\IndexController@upload')->name('upload');

Laravel控制器中处理上传的方法:

<?php // Web/IndexController.phpfunction upload() {    dump(request());}

nginx-upload-progress-module

nginx-upload-progress-module可以帮助我们跟踪上传的进度,使用方法见下。

NginX的站点配置:

# website.confserver {    ...    client_max_body_size 32m;    # 开辟一个空间proxied来存储跟踪上传的信息1MB    upload_progress proxied 1m;    location ^~ /progress {        # 报告上传的信息        report_uploads proxied;    }    location /upload {        ...        # 上传完成后,仍然保存上传信息5s        track_uploads proxied 5s;    }}

上传文件的页面和每隔一秒查询一下上传进度的脚本:

<form id="upload" enctype="multipart/form-data" action="/upload" method="post" onsubmit="openProgressBar(); return true;">    <input name="userfile" type="file" label="fileupload" />    <input type="submit" value="Upload File" />form><div>    <div id="progress" style="width: 400px; border: 1px solid black">        <div id="progressbar" style="width: 1px; background-color: black; border: 1px solid white"> div>    div>   <div id="tp">(progress)div>div><script type="text/javascript">    var interval = null;    var uuid = "";    function openProgressBar() {        for (var i = 0; i < 32; i++) {            uuid += Math.floor(Math.random() * 16).toString(16);        }        document.getElementById("upload").action = "/upload?X-Progress-ID=" + uuid;        /* 每隔一秒查询一下上传进度 */        interval = window.setInterval(function () {            fetch(uuid);        }, 1000);    }    function fetch(uuid) {        var req = new XMLHttpRequest();        req.open("GET", "/progress", 1);        req.setRequestHeader("X-Progress-ID", uuid);        req.onreadystatechange = function () {            if (req.readyState == 4) {                if (req.status == 200) {                    var upload = eval(req.responseText);                    document.getElementById('tp').innerHTML = upload.state;                    /* 更新进度条 */                    if (upload.state == 'done' || upload.state == 'uploading') {                        var bar = document.getElementById('progressbar');                        var w = 400 * upload.received / upload.size;                        bar.style.width = w + 'px';                    }                    /* 上传完成,不再查询进度 */                    if (upload.state == 'done') {                        window.clearTimeout(interval);                    }                    if (upload.state == 'error') {                        window.clearTimeout(interval);                        alert('something wrong');                    }                }            }        }        req.send(null);    }script>

当上传文件的体积大于client_max_body_size时, nginx-upload-module未能帮我们立刻中断上传,并且不能限制上传速度,但是nginx-upload-progress-module可以向前端报告文件过大的错误,前端可以这样子来中断上传:

<form id="upload" enctype="multipart/form-data" action="/upload" method="post" onsubmit="openProgressBar(); return false;">    <input name="userfile" type="file" label="fileupload" id="userfile" />    <input type="submit" value="Upload File" />form><div>    <div id="progress" style="width: 400px; border: 1px solid black">        <div id="progressbar" style="width: 1px; background-color: black; border: 1px solid white"> div>    div>   <div id="tp">(progress)div>div><script type="text/javascript">    var interval = null;    var uuid = "";    var uploadxhr = null;     function openProgressBar() {        for (var i = 0; i < 32; i++) {            uuid += Math.floor(Math.random() * 16).toString(16);        }        var action = "/upload?X-Progress-ID=" + uuid;        var file = document.getElementById('userfile').files[0];        uploadxhr = new XMLHttpRequest();        // uploadxhr.file = file;        uploadxhr.open('post', action, true);        uploadxhr.setRequestHeader("Content-Type","multipart/form-data");        uploadxhr.send(file);        /* 每隔一秒查询一下上传进度 */        interval = window.setInterval(function () {            fetch(uuid);        }, 1000);    }    function fetch(uuid) {        var req = new XMLHttpRequest();        req.open("GET", "/progress", 1);        req.setRequestHeader("X-Progress-ID", uuid);        req.onreadystatechange = function () {            if (req.readyState == 4) {                if (req.status == 200) {                    var upload = eval(req.responseText);                    document.getElementById('tp').innerHTML = upload.state;                    /* 更新进度条 */                    if (upload.state == 'done' || upload.state == 'uploading') {                        var bar = document.getElementById('progressbar');                        var w = 400 * upload.received / upload.size;                        bar.style.width = w + 'px';                    }                    /* 上传完成,不再查询进度 */                    if (upload.state == 'done') {                        window.clearTimeout(interval);                    }                    if (upload.state == 'error') {                        window.clearTimeout(interval);                        uploadxhr.abort();                        alert('something wrong');                    }                }            }        }        req.send(null);    }script>

另外

nginx-upload-module和nginx-upload-progress-module还提供了更多的指令,帮忙我们实现更复杂的上传文件功能,比如断点续传等,有兴趣可以阅读两个模块的官方文档,了解更多。

另外,因为nginx-upload-module未能及时拦下体积过大的文件上传,所以,尽管保障了用户的正常使用,可是依然不能防范恶意的流量攻击。

nginx-upload-progress-module能够在一开始就检测到上传文件的体积是否过大(HTTP请求头里的Content-Length存有文件的体积大小),这时候就应该中断上传(可能是NginX限制,扩展模块无法中断HTTP请求),大家有兴趣的话可以研究一下NginX源码和扩展开发。

思考

NginX的client_max_body_size设为32m,攻击者可以上传1GB的文件,直到上传到32MB的时候,NginX才会中断上传,服务器被消耗了32MB的流量。细想一下:

  1. 即使NginX在一开始就拦下了体积大于32MB的文件,可是攻击者依然可以直接上传30MB大小的文件,服务器还是会被消耗了30MB的流量,所以在一开始就拦截的意义并不大;

  2. 可是上传文件的体积大于client_max_body_size时,nginx-upload-module的限速功能不起作用,这就成问题了;

  3. NginX没有直接信任请求头的Content-Length,应该有他的依据,不过正常用户不会虚报吧(即使报小也不报大啊);

  4. 看来这个方案还需继续完善,或者借助现成的云存储服务来实现文件上传功能(可参考腾讯云COS的一次实践)。

最后

如果一个网站,允许用户全速上传文件,并持续数十秒,那么这个网站一定存在被流量攻击的风险,有可能是大量用户同时使用造成的,也有可能是恶意的DDoS攻击(??好像所有网站都会有这个风险)。

要是服务器带宽被占满,服务器对于一些用户就像是掉线了,所以上传文件的问题必须重视。另外,开发者不应该局限于一种编程语言或者一个知识领域上去思考解决问题,应该涉览更多的知识领域,从更多角度、更多方位去解决问题。

- EOF -

推荐阅读  点击标题可跳转

1、Nginx 一个牛X的功能,流量拷贝!

2、PHP下kafka的实践

3、Nginx 正向代理与反向代理

看完本文有收获?请分享给更多人

推荐关注「PHP开发者」,提升PHP技能

8c9fc0aff5fec71da4934296f5f7409c.png

点赞和在看就是最大的支持❤️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/335927.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高通写号工具_高通推出桌面平台新ARM处理器并认为我们的电脑性能没必要那么高...

高通公司在日前举办的骁龙技术峰会上宣布推出骁龙7c / 8c处理器 , 这些处理器全部都是面向笔记本电脑推出的。这也是高通和微软合作推出 Windows 10 ARM 设备的组成部分 , 高通希望能够在桌面平台挑战英特尔统治地位。如果你有印象的话或许还记得高通此前推出的骁龙 8cx芯片组&…

混合高斯模型_高斯混合模型(GMM)

下图所示&#xff0c;显然用右边的图描述当前分布更加合理&#xff0c;即应用了两个高斯分布。图中每一个样本点同时属于任何一个高斯模型。高斯混合模型 从几何角度来理解&#xff0c;GMM是由多个高斯分布叠加而成&#xff0c;可以看做是多个高斯分布的加权平均。其中&#x…

来的多可选_您的框架有多可扩展性?

来的多可选在参加会议时&#xff0c;我们总是会遇到高素质的决策者&#xff0c;他们经常问同样的问题&#xff1a; 您的框架有多可扩展性&#xff1f;如果我需要的比您开箱即用的功能还多呢&#xff1f; 。 这个问题非常合理&#xff0c;因为他们只是不想被卡在开发曲线的中间&…

【H.264/AVC视频编解码技术】第六章【指数哥伦布编码】

H264中语法元素描述符 指数哥伦布 (Exponential-Golomb) 熵编码 指数哥伦布编码同哈夫曼编码一样,都是变长编码。 二者的显著区别: 信源相关性:哈夫曼编码依赖于信源的概率分布;指数哥伦布与信源无关。 额外信息:哈夫曼编码的数据必须额外携带与信源匹配的码表;指…

C++ 11 深度学习(七)位运算常见操作

1. 取出数中任意k位置的二进制位是0还是1 n >> k & 1 原理&#xff1a;先把想要取出的位置移动到个位&#xff0c;1的二进制是 0001 , 进行与操作就可以提取出最后一位是0还是1&#xff1b;二进制是从右向左&#xff0c;由低到高&#xff0c;从0到7。 2. 右移操作 …

聚合项目访问后台接口失败_聚合支付系统和免签支付系统对未来支付市场有哪些影响...

时势所趋&#xff0c;在如今支付通道不稳定的情况下&#xff0c;四方聚合支付的出现弥补了通道不稳的情况&#xff0c;四方聚合支付可以接入多个三方&#xff0c;实现在三方不稳的情况直接后台切换三方&#xff0c;实现一秒切换&#xff0c;还可以接入个人免签支付系统&#xf…

activemq消息持久化_ActiveMQ 5.x中的消息持久性

activemq消息持久化我被问了很多关于ActiveMQ如何存储消息&#xff08;或在某些情况下不存储&#xff09;的基本知识。 这是它的高级解释。 注意&#xff0c;上下文在JMS中。 如果您使用ActiveMQ的非JMS客户端&#xff08;即STOMP&#xff0c;AMQP&#xff0c;MQTT等&#xff0…

【安卓开发 】Android初级开发(十一)Android中多线程

线程的创建 1.创建一个Thread类&#xff0c;或者创建一个Thread子的对象&#xff1b; 2.创建一个Runnable接口的类对象; 传入Runnable对象创建线程 package com.sina.baode;import android.util.Log;/** 自定义一个继承于Runnable*/public class DemoThread implements Run…

td 内单选框不可用_在TD,我和曾经的老师变成了同事,也收获了最满意的“课外活动”...

早就想找个时间好好写一篇文章记录一下我在TD的学习工作经历了&#xff0c;同时表达一下我对TD深深的爱和感谢。一、结缘TD一年前&#xff0c;我第一次报了TD的AP环境科学寒假班。由于当时我在冲刺三月的SAT&#xff0c;整个寒假我一心扑在了SAT上&#xff0c;没来得及听TD的直…

activiti脚本任务_Activiti中的安全脚本如何工作

activiti脚本任务最近的Activiti 5.21.0版本的突出特点之一是“安全脚本”。 Activiti用户指南中详细介绍了启用和使用此功能的方法 。 在这篇文章中&#xff0c;我将向您展示我们如何实现其最终实现以及它在幕后所做的事情。 当然&#xff0c;因为这是我通常的签名风格&#x…

C++ 11 深度学习(八)重定义override

1.动态联编&#xff0c;本质是在运行时多态的表现。 2.其本质是因为维护了一张虚函数表&#xff0c;虚函数表以链表的形式存在。每个结点存储了对象指针的地址&#xff0c;通过一个指针进行遍历索引。 #include <iostream> #include <armadillo> using namespace …

逻辑回归模型_联邦学习体系下——逻辑回归模型

联邦学习的体系我们在前期介绍过&#xff0c;这里我们简单回顾一下纵向联邦学习的定义&#xff1a;在两个数据集的用户重叠较多而用户特征重叠较少的情况下&#xff0c;将数据集按照纵向 (即特征维度)切分&#xff0c;并取出双方用户相同而用户特征不完全相同的那部分数据进行训…

C++ 11 深度学习(九)C++文件IO

1.将数据写入文件 #include <iostream> #include <fstream> using namespace std;int main() {ofstream p1;p1.open("outfile.txt");p1 << "向文件写入信息" << endl;p1.close();return 0; } 2.将数据从文件中读出 #inclu…

plsql例外_大例外背后的真相

plsql例外异常可能是最被滥用的Java语言功能。 这就是为什么 让我们打破一些神话。 没有牙仙子。 圣诞老人不是真实的。 TODO评论。 finalfinalversion-final.pdf。 无皂肥皂。 而且…例外实际上是例外。 后者可能需要更多说服力&#xff0c;但是我们可以帮助您。 在这篇文章…

滴滴java开发面试题_Java开发经典面试题(十二)

好久没有来更新我的面试题了&#xff0c;不知道关注我的小伙伴有没有失联啊&#xff1f;&#xff01;呼叫&#xff01;好了开始我们今天的正题分享&#xff01;1、如何从FutureTask不阻塞获取结果get(long timeout,TimeUnit unit)&#xff0c;超时则返回轮询&#xff0c;先通过…

hashmap大小_调整HashMap的大小:未来的危险

hashmap大小最近&#xff0c;我偶然发现了一个错误&#xff0c;该错误是由于多个线程对java.util.HashMap的使用不当引起的。 该错误是泄漏抽象的一个很好的例子。 只有了解数据结构的实现级别详细信息&#xff0c;才能帮助我解决当前的问题。 因此&#xff0c;我希望与他人分享…

apache spark_Apache Spark软件包,从XML到JSON

apache sparkApache Spark社区为扩展Spark付出了很多努力。 最近&#xff0c;我们希望将XML数据集转换为更易于查询的内容。 我们主要对每天进行的数十亿笔交易之上的数据探索感兴趣。 XML是一种众所周知的格式&#xff0c;但是有时使用起来可能很复杂。 例如&#xff0c;在Apa…

【OpenGL从入门到精通(七)】OpenGL中的数学

1.向量单位化 2.三维向量点乘/点积&#xff08;结果为标量&#xff09; 3.三维向量叉乘&#xff08;叉积&#xff09;结果为向量 3.坐标平移 因为在OpenGL中使用的都是齐次坐标&#xff0c;即x , y , z , w 如果使得点(0, 0, 0) 平移到(1, 2, 3)位置。将坐标表示为矩阵的形式&…

javafx窗体程序_JavaFX实际应用程序:SkedPal

javafx窗体程序“真实世界的应用程序”系列中的一个新条目。 这次是SkedPal &#xff0c;这是一个用于智能管理忙人生活的应用程序。 我一直在咨询SkedPal团队有关JavaFX的事务&#xff0c;并且在他们决定开始使用我的CalendarFX框架来满足他们的日历要求时&#xff0c;我也在咨…

kafka 发布订阅_在Kafka中发布订阅模型

kafka 发布订阅这是第四个柱中的一系列关于同步客户端集成与异步系统&#xff08; 1&#xff0c; 2&#xff0c; 3 &#xff09;。 在这里&#xff0c;我们将尝试了解Kafka的工作方式&#xff0c;以便正确利用其发布-订阅实现。 卡夫卡概念 根据官方文件 &#xff1a; Kafka是…