笔者最近写了一个通过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="上传文件" onclick="Upload()" style="width: 96px; height: 32px;"/>
<input type="button" ID="bnClearFile" value="清空文件" onclick="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访问。当然,可以与网管协商放开防火墙做测试看看。不过防火墙由用户方控制,涉及到云服务等的安全,一般不会放开。
碰到一堵墙时,可以找人开个口子,也可以搭个梯子翻过去。呵呵,笔者采用了后一种方法。