笔者最近写了一个通过ASP.NET MVC4 WebApi、jQuery、ajax和FormData上传文件的系统(见基于ASP.NET MVC4、WebApi、jQuery和FormData的多文件上传方法),在自己的笔记本上测试一切正常,但发布到客户服务器(云服务器,Windows Server 2012 操作系统,有很强防火墙保护)时,部分文件上传正常,但多数文件上传抛出如下异常信息(被jquery的ajax 的 error 捕获):NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load http://xxx/Api/FilesApi/Upload。这里,Upload是路由api、控制器FilesApi的Post方法。
网查了许多方法,一般说是跨域调用引起的异常,但使用介绍的相关方法都没有解决该问题。笔者估计是防火墙检查了通过http传入的文件内容以及文件名,或者是浏览器有很强的内容过滤功能,发现它认为有威胁的关键字符串,就拒绝调用相关Api方法。为此,笔者想到了通过html5引入的FileReader对象(FormData也是html5引入),在客户端使用该对象的异步方法readAsURLData获取文件内容的base64加密文本(注意,FileReader.readAsURLData()方法使用了UTF-8编码做base64加密),然后通过FormData发送该文本,在服务器WebApi中解密该文本,然后保存文件。同样,使用了一个base64加密的js插件,加密上传的文件名。
如下是Home控制器对应的客户端脚本(仅上传两个文件)Index.cshtml:
@model CSUST.Files.TDirItems @{ Layout = null; } <!DOCTYPE html> <html> <head> <title>文件上传</title> <script type="text/javascript" src='@Url.Action("jquery-1.12.4.min.js", "scripts")'></script> <script type="text/javascript" src='@Url.Action("jquery.base64.min.js", "scripts")'></script> <script type="text/javascript"> var fileData = []; $(document).ready(function () { $("#tbFileName1").on("change", function(){ GetFile(this); }); $("#tbFileName2").on("change", function () { GetFile(this); }); }); function GetFile(fileObj) { fileData[fileObj.id] = null; var fileName = $(fileObj).val(); if (fileName == null || fileName == "") // 文件名为空 { return; } var files = $(fileObj).get(0).files; if (files[0].size > 4 * 1024 * 1024) { alert("单个文件不能大于4M。"); $(fileObj).val(""); return; } var reader = new FileReader(); reader.readAsDataURL(files[0]); reader.onload = function () { var base64 = reader.result; var n = base64.indexOf("base64,"); fileData[fileObj.id] = base64.substr(n + 7); } } function Clear() { $("#tbFileName1").val(""); $("#tbFileName2").val(""); } function Upload() { var f1 = $("#tbFileName1").val(); var f2 = $("#tbFileName2").val(); if ((f1 == null || f1 == "") && (f2 == null || f2 == "")) { alert("至少要上传一个文件。"); return; } if (f1 != "" && fileData["tbFileName1"] == null) { alert("尚未读取文件1,稍后!"); return; } if (f2 != "" && fileData["tbFileName2"] == null) { alert("尚未读取文件2,稍后!"); return; } if(f1 == f2) { alert("不能上传相同文件。"); return; } var n1 = $("#tbTicket").val(); var n2 = $("#ckAllowNewFiles").is(":checked"); var n3 = $("#cbDirNameKeys").val(); if (n1 == "") { alert("必须输入验证口令。"); return; } if (n3.indexOf("\\") == 0) { alert("不能选择\\注释格式的文件夹项。"); return; } var formData = new FormData(); formData.append("VerifyTicket", n1); formData.append("AllowNewFiles", n2); formData.append("DirNameKey", n3); if (f1 != "") { formData.append("FileName1", $.base64('encode', f1)); formData.append("FileContent1", fileData["tbFileName1"]); } if (f2 != "") { formData.append("FileName2", $.base64('encode', f2)); formData.append("FileContent2", fileData["tbFileName2"]); } SendFiles(formData); } function SendFiles(formData) { $.ajax({ type: "post", url: '@Url.Action("Upload", "Api/FilesApi")', async: false, data: formData, contentType: false, processData: false, success: function (data, status) { if (status != "success") { alert("上传文件失败: " + status); return; } if (data == null) { alert("上传文件失败, 没有返回结果。"); return; } if (data.IsFailed == true) { alert(data.ErrorMessage); return; } alert(data.Note); $("#tbFileName1").val(""); $("#tbFileName2").val(""); }, error: function (xhr, status, err) { alert("上传文件异常: " + status + "," + err); } }); } </script> </head> <body> <form id="Form1"> <div align="center"> <h2><label>@Model.WebSiteTitle</label></h2> <table style="width: 1050px;" border="1"> <tr style="height: 32px"> <td rowspan="2" style="width: 120px;text-align:center"> 文件名 </td> <td colspan="2" align="left"> <input ID="tbFileName1" name="tbFileName1" type="file" style="width: 96%;"/> </td> </tr> <tr style="height: 32px"> <td colspan="2" align="left"> <input ID="tbFileName2" name="tbFileName2" type="file" style="width: 96%;"/> </td> </tr> <tr style="height: 42px"> <td style="text-align: center">到文件夹</td> <td style="width: 650px; text-align: left;"> <select ID="cbDirNameKeys" style="width: 650px;"> @foreach(CSUST.Files.TDirItem item in Model.DirItems) { <option>@item.DirNameKey</option> } </select> </td> <td style="width: 280px; text-align:left;"> <input type="checkbox" ID="ckAllowNewFiles" /> <label for="ckAllowNewFiles">新增cshtml.css.js.jpg等文件</label> </td> </tr> <tr style="height: 42px;"> <td style="height: 42px; text-align: center;">校验口令</td> <td style="height: 42px; text-align: left;"> <input type="password" id="tbTicket" style="width: 645px;" /> </td> <td style="height: 42px; text-align:center;"> <input type="button" ID="bnUpload" value="上传文件" οnclick="Upload()" style="width: 96px; height: 32px;"/> <input type="button" ID="bnClearFile" value="清空文件" οnclick="Clear()" style="width: 96px; height: 32px;" /> </td> </tr> </table> </div> </form> </body> </html>如下是服务器端ASP.NET MVC4的WebApi对应的POST方法Upload: using System; using System.Collections.Generic; using System.Web; using System.Web.Http; namespace CSUST.Files { public class FilesApiController : ApiController { [HttpPost] public CSUST.Web.TWebApiResult Upload() { try { var httpRequest = HttpContext.Current.Request; var dirNameKey = httpRequest.Form["DirNameKey"]; var allowNewFiles = httpRequest.Form["AllowNewFiles"]; var verifyTicket = httpRequest.Form["VerifyTicket"]; if (dirNameKey.StartsWith(TDirItem.NoteChars) == true) { return new Web.TWebApiResult("不能选择" + TDirItem.NoteChars + "注释格式的文件夹项。"); } TDirItems dirItems = new TDirItems(); if (dirItems.VerifyTicket != verifyTicket) { return new Web.TWebApiResult("验证口令错误。"); } List<string> fileNames = new List<string>(); List<string> fileContents = new List<string>(); if (string.IsNullOrWhiteSpace(httpRequest.Form["FileName1"]) == false) { string fileName1 = this.GetFileNameByBase64(httpRequest.Form["FileName1"]); fileNames.Add(fileName1); fileContents.Add(httpRequest.Form["FileContent1"]); } if (string.IsNullOrWhiteSpace(httpRequest.Form["FileName2"]) == false) { string fileName2 = this.GetFileNameByBase64(httpRequest.Form["FileName2"]); fileNames.Add(fileName2); fileContents.Add(httpRequest.Form["FileContent2"]); } CSUST.Text.TStringItemsBuilder sb = new Text.TStringItemsBuilder(Environment.NewLine); foreach (var fn in fileNames) { if (dirItems.IsAllowedFileName(dirNameKey, fn) == false) { sb.AppendItem(fn + "不能保存到指定的文件夹中。"); } var saveFileName = dirItems.GetSavedFileName(dirNameKey, fn); if (System.IO.File.Exists(saveFileName) == false && allowNewFiles.ToUpper() != "TRUE") { sb.AppendItem(fn + "新文件时必须勾选新增复选框。"); } } if (sb.Length > 0) { return new Web.TWebApiResult(sb.ToString()); } sb.Clear(); sb.AppendItem("保存文件成功:"); for (int k = 0; k < fileNames.Count; k++) { var saveFileName = dirItems.GetSavedFileName(dirNameKey, fileNames[k]); byte[] fb = Convert.FromBase64String(fileContents[k]); using (System.IO.FileStream fs = new System.IO.FileStream(saveFileName, System.IO.FileMode.Create, System.IO.FileAccess.Write)) { fs.Write(fb, 0, fb.Length); fs.Flush(); sb.AppendItem(saveFileName); } } CSUST.Web.TWebApiResult r = new Web.TWebApiResult() { Note = sb.ToString() }; return r; } catch (Exception err) { return new CSUST.Web.TWebApiResult(err, true); } } private string GetFileNameByBase64(string base64FileName) { byte[] b = Convert.FromBase64String(base64FileName); string fileName = System.Text.Encoding.UTF8.GetString(b); // 前端使用了base64加密,防止文本串被防火墙拒绝 return System.IO.Path.GetFileName(fileName); } } }经过上述技术处理后提交一般文件正常,但在上传Global.asax文件时,仍然抛出上述异常。测试时把该文件改名为@@Global.asax则可正常上传。显然,浏览器或防火墙把Global.asax作为威胁拒绝了(笔者估计是浏览器拒绝了上传提交)。
目前看,问题部分获得解决。但是否还有加密后的文本被防火墙或浏览器视为威胁,不得而知。根本上,目前还不清楚到底是浏览器还是防火墙或Windows Server2012拒绝了WebApi访问。当然,可以与网管协商放开防火墙做测试看看。不过防火墙由用户方控制,涉及到云服务等的安全,一般不会放开。
碰到一堵墙时,可以找人开个口子,也可以搭个梯子翻过去。呵呵,笔者采用了后一种方法。
