ASP.NET WebApi 上传文件时异常 Failed to execute send on XMLHttpRequest 的一个处理方法

笔者最近写了一个通过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访问。当然,可以与网管协商放开防火墙做测试看看。不过防火墙由用户方控制,涉及到云服务等的安全,一般不会放开。

碰到一堵墙时,可以找人开个口子,也可以搭个梯子翻过去。呵呵,笔者采用了后一种方法。

猜你喜欢

转载自blog.csdn.net/hulihui/article/details/71227344
今日推荐