對于微云、百度云等網盤提供的文件存儲服務而言,文件上傳是一個重要功能。文件上傳的方式主要有兩種:二進制數據上傳、表單上傳。本文會詳細解析表單上傳的協議規范,前端上傳文件的兩種方式:對話框選擇方式、拖拽選擇方式,服務端接收上傳的文件以及文件上傳功能的技巧等。
RFC1867(https://www.ietf.org/rfc/rfc1867.txt) 規范了表單上傳的協議格式。下面給出一個例子,用Fiddler抓包工具,抓取同時上傳兩個字符串內容和一個文本文件的HTTP請求,獲取的請求內容如下:
POST http://localhost:8080/Server/uploadfile HTTP/1.1Host: localhost:8080Connection: keep-aliveContent-Length: 391Cache-Control: no-cacheOrigin: Chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcmUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5Accept: */*Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.8------WebKitFormBoundaryuA8AsEvrgV5BUqe5Content-Disposition: form-data; name="file1"value1------WebKitFormBoundaryuA8AsEvrgV5BUqe5Content-Disposition: form-data; name="file2"; filename="test2.txt"Content-Type: text/plainhello world------WebKitFormBoundaryuA8AsEvrgV5BUqe5Content-Disposition: form-data; name="file3"value3------WebKitFormBoundaryuA8AsEvrgV5BUqe5--根據HTTP協議規范,每個請求頭后面都需要追加回車和換行符(/r/n)。消息頭和消息體之間也需要插入回車和換行符,忽略其它的請求頭部,表單上傳的格式可簡化成如下代碼,方便描述。
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行回車換行------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行Content-Disposition: form-data; name="file2"; filename="test2.txt"回車換行Content-Type: text/plain回車換行回車換行hello world回車換行------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行Content-Disposition: form-data; name="file3"回車換行回車換行value3回車換行------WebKitFormBoundaryuA8AsEvrgV5BUqe5--回車換行使用表單上傳功能,需要在頭部添加如下代碼,其中“multipart/form-data”表示請求上傳的內容類型為表單,“boundary”表示分隔符,用于分割表單里面的每項內容。
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行表單中每項內容的類型無外乎就兩種,一種是文本類型,另外一種是文件類型。每項內容之間需要用“–+boundary+回車換行”進行分割,緊接著分隔符的代碼用于描述內容配置。其中文本類型的內容需要添加如下格式的代碼:
------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行Content-Disposition: form-data; name="file3"回車換行回車換行value3回車換行“name”用于描述表單的字段名稱,兩個回車換行之后就是這個字段的值。文件類型的內容跟文本類型對比多了兩個字段,“filename”用于描述上傳的文件的名稱,“Content-Type”用于描述上傳的文件類型(文件的MIME),文件類型的內容需要添加如下格式的代碼:
------WebKitFormBoundaryuA8AsEvrgV5BUqe5回車換行Content-Disposition: form-data; name="file2"; filename="test2.txt"回車換行Content-Type: text/plain回車換行回車換行hello world回車換行添加完表單的每項內容之后,需要在后面追加“–+boundary+–+回車換行”,完成表單內容的拼接。
實現對話框選擇文件,會用到如下代碼:
<form action="http://localhost:8080/Server/uploadfile" method="post" enctype="multipart/form-data"> <br> 文件: <input type="file" name="image"> <br> <input type="submit" value="上傳"> </form>其中action字段為文件上傳的接口地址,enctype需要定義為“multipart/form-data”、input標簽的type屬性的值為“file”,對應的name為表單的字段名稱。
要實現這個功能,可借助Html5新增的“Drag and drop”功能。W3C官方文檔為:https://www.w3.org/TR/2014/CR-html5-20140731/editing.html。 利用它,我們可以知道文件何時被拖動到目標區域、文件何時離開目標區域、有哪些文件被拖到了目標區域。接下來就具體聊聊“Drag and drop”功能。
HTML中的每個標簽都能夠設置跟拖動相關的事件,拖動事件的回調函數解釋如下:
事件 | 描述 |
---|---|
ondragstart | 拖動操作開始時調用(部分瀏覽器不回調此方法) |
ondrag | 拖動過程中調用(部分瀏覽器不回調此方法) |
ondragenter | 剛拖動到目標元素區域時調用 |
ondragover | 在目標元素區域內拖動時調用,此方法會隔一段時間調用一次 |
ondragleave | 拖動離開目標元素區域時調用 |
ondragend | 拖動結束時回調(部分瀏覽器不回調此方法) |
ondrop | 在目標元素區域內放開拖動內容時調用 |
注冊事件可以使用如下代碼:
//element可以為HTML標簽、documentelement.ondragstart = function(ev) { console.log('ondragstart');}注意:
瀏覽器默認在拖放完成時會打開所拖放的文件,正確的做法是要調用事件對象的PReventDefault方法用來阻止事件的默認動作的執行。
//element可以為HTML標簽、documentelement.ondragover = function(ev) { ev.preventDefault(); //do something}上面所列舉的回調函數,每個回調函數里面都有一個參數DragEvent,DragEvent的接口定義語言描述如下:
interface DragEvent : MouseEvent { readonly attribute DataTransfer? dataTransfer;};可以看到拖動事件接口繼承于鼠標事件接口,其中有個屬性dataTransfer(數據傳輸者)用于傳輸拖動的內容,DataTransfer的接口定義語言如下:
interface DataTransfer { attribute DOMString dropEffect; attribute DOMString effectAllowed; readonly attribute DataTransferItemList items; void setDragImage(Element image, long x, long y); /* old interface */ readonly attribute DOMString[] types; DOMString getData(DOMString format); void setData(DOMString format, DOMString data); void clearData(optional DOMString format); readonly attribute FileList files;};其中的setData方法用于設置要傳輸的內容,getData方法用于獲取傳輸的內容。當要實現從一個元素中拖動內容到另外一個元素區域時可以使用者兩個方法。拖動文件時值需要使用files屬性,其值被瀏覽器設置進去了,因此只要獲取即可。那么獲取files的最佳時機是什么時候,當然是在ondrop方法回調時最佳。
dz.ondrop = function(ev) { //阻止瀏覽器默認打開文件的操作 ev.preventDefault(); //表單上傳文件...}使用Ajax即可通過表單方式上傳文件,附上前端拖拽上傳的完整代碼。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN"><head> <title>HTML5拖拽上傳</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="description" content="" /> <meta name="keyWords" content="" /> <style type="text/CSS"> #dropzone { width: 300px; height: 300px; border: 2px dashed gray; } #dropzone.over { width: 300px; height: 300px; border: 2px dashed red; } </style></head><body> <div id="dropzone" dropEffect="link"></div></body><script type="text/javascript">function uploadFile(formData) { var xhr = new xmlhttpRequest(); xhr.open('POST', 'http://localhost:8080/Server/uploadfile', true); xhr.send(formData);}var dz = document.getElementById('dropzone');dz.ondragover = function(ev) { //阻止瀏覽器默認打開文件的操作 ev.preventDefault(); this.className = 'over';}dz.ondragleave = function() { this.className = '';}dz.ondrop = function(ev) { this.className = ''; //阻止瀏覽器默認打開文件的操作 ev.preventDefault(); //表單上傳文件 var formData = new FormData(); formData.append('file', ev.dataTransfer.files[0]); uploadFile(formData);}</script></html>服務端代碼本人采用JAVAEE開發,文件上傳使用到commons fileupload組件:https://commons.apache.org/proper/commons-fileupload/download_fileupload.cgi,commons fileupload依賴common io庫。
完整代碼如下:
package com.servlet;import java.io.File;import java.io.IOException;import java.util.Iterator;import java.util.List;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.commons.fileupload.FileItem;import org.apache.commons.fileupload.FileUploadException;import org.apache.commons.fileupload.disk.DiskFileItemFactory;import org.apache.commons.fileupload.servlet.ServletFileUpload;@WebServlet(name = "uploadfile", urlPatterns = "/uploadfile")public class UploadFileServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { ServletContext servletContext = this.getServletConfig() .getServletContext(); // Create a factory for disk-based file items DiskFileItemFactory factory = new DiskFileItemFactory(); // Set factory constraints String path = "D://upload"; File uploadDir = new File(path); if (!uploadDir.exists()) { uploadDir.mkdirs(); } factory.setRepository(new File(uploadDir.getAbsolutePath())); // Create a new file upload handler ServletFileUpload upload = new ServletFileUpload(factory); // Set overall request size constraint upload.setSizeMax(-1); // Parse the request List<FileItem> items = upload.parseRequest(req); // Process the uploaded items Iterator<FileItem> iter = items.iterator(); while (iter.hasNext()) { FileItem item = iter.next(); if (item.isFormField()) { // 普通表單數據 } else { // 文件表單數據 item.write(new File(uploadDir.getAbsolutePath() + File.separator + item.getName())); } } } catch (Exception e) { e.printStackTrace(); } }}微云、百度云就含有文件秒傳功能,其實現原理其實很簡單,文件可以用其md5來區分差異性。上傳文件時計算文件的MD5,只要服務器上存在相同MD5值的文件,則不會真正的上傳文件,而是把網盤上文件的索引存儲到當前用戶信息中。所以一般網盤上不會出現MD5值相同的文件。
以tomcat服務器為例,WEB-INF目錄可以被瀏覽器訪問。如果用戶將可執行的文件如xx.jsp上傳到這個目錄,里面編寫了刪除文件目錄的代碼,則當瀏覽器訪問這個xx.jsp文件時,這段惡意代碼就會被執行,這顯然是惡意攻擊。為了阻止這種行為,正確的做法是過濾掉可執行文件,不讓其上傳,這種判斷前端和后端都需要做,前端做的目的是可以減輕服務端的判斷壓力,后端做是為了阻止模擬的HTTP請求上傳惡意文件。
長按下圖->識別圖中二維碼或者掃一掃關注我的公眾號。
新聞熱點
疑難解答