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_filesize和post_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的流量。細想一下:
即使NginX在一開始就攔下了體積大于32MB的文件,可是攻擊者依然可以直接上傳30MB大小的文件,服務器還是會被消耗了30MB的流量,所以在一開始就攔截的意義并不大;
可是上傳文件的體積大于client_max_body_size時,nginx-upload-module的限速功能不起作用,這就成問題了;
NginX沒有直接信任請求頭的Content-Length,應該有他的依據,不過正常用戶不會虛報吧(即使報小也不報大啊);
看來這個方案還需繼續完善,或者借助現成的云存儲服務來實現文件上傳功能(可參考騰訊云COS的一次實踐)。
最后
如果一個網站,允許用戶全速上傳文件,并持續數十秒,那么這個網站一定存在被流量攻擊的風險,有可能是大量用戶同時使用造成的,也有可能是惡意的DDoS攻擊(??好像所有網站都會有這個風險)。
要是服務器帶寬被占滿,服務器對于一些用戶就像是掉線了,所以上傳文件的問題必須重視。另外,開發者不應該局限于一種編程語言或者一個知識領域上去思考解決問題,應該涉覽更多的知識領域,從更多角度、更多方位去解決問題。
- EOF -
推薦閱讀??點擊標題可跳轉1、Nginx 一個牛X的功能,流量拷貝!
2、PHP下kafka的實踐
3、Nginx 正向代理與反向代理
看完本文有收獲?請分享給更多人
推薦關注「PHP開發者」,提升PHP技能
點贊和在看就是最大的支持??
總結
以上是生活随笔為你收集整理的nginx delete form表单 收不到参数_HTTP 文件上传的一个后端完善方案(NginX)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ai导出单个画板快捷键(ai复制到全部画
- 下一篇: 光圈优先快捷键(光圈优先快捷键怎么设置)