Web Api won't download file using jQuery Ajax and Basic Auth

徘徊边缘 提交于 2020-01-29 05:37:04

问题


I am using the ASP.NET Web API to build a prototype of a web service (and site) which has a method to download a file. When the user on the front-end presses the export button a jQuery ajax GET request is made and received by the controller which on it's turn calls the method named Excel (shown below). The method runs without any problem and finishes. When I look in Chrome at the header (see https://skydrive.live.com/redir?resid=2D85E5C937AC2BF9!77093) it receives the response with (as far as I am concerned) all the right headers.

I am using Basic Auth, so the user credentials are transmitted using the http authorization header which I have manually added to every jQuery Ajax request using Ajax Options.

var excelRequest = $.ajax({
    url: 'http://localhost:59390/api/mycontroller/excel',
    cache: false,
    type: 'GET',
    data: gridString,
    dataType: 'json',
    contentType: 'application/json; charset=utf-8'
});

$.ajaxSetup({
    beforeSend: function (xhr) {
        SetAuthRequestHeader(xhr)
    }
});

function SetAuthRequestHeader(jqXHR) {
    var usr = "Gebruiker2"; // TODO: Change, this is for testing only.
    var pw = "Wachtwoord23";
    jqXHR.setRequestHeader("Authorization", "Basic " + Base64.encode(usr + ":" + pw));
}

My prototype has some characteristics which I should mention:

  • Uses Basic Auth in Authorization header

  • The web service and the web site which calls the service are on different domains, so I used CORS and added the following to the web.config

<httpProtocol>
    <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="access-control-allow-headers" value="Content-Type, Authorization, Accept" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" 
    </customHeaders>
</httpProtocol>

Shown below is the entire Excel method.

    [HttpGet]
    // Get api/myController/excel 
    public HttpResponseMessage Excel(string sidx, string sord, int page, int rows, string Depot, string PDID, string User, string Property, string Value)
    {
        if (AuthHelper.AuthService.HasValidCredentials(Request))
        {
            var gridResult = this.GetDataJqGridFormat( sidx,  sord,  page,  rows,  Depot,  PDID,  User,  Property,  Value);

            // Generate a HTML table.
            StringBuilder builder = new StringBuilder();
            // We create a html table:
            builder.Append("<table border=1>");
            builder.Append("<tr><td>DEPOT</td>");
            builder.Append("<td>PDID</td>");
            builder.Append("<td>USER</td>");
            builder.Append("<td>PROPERTY</td>");
            builder.Append("<td>VALUE</td></tr>");
            // Create response from anonymous type            
            foreach (var item in gridResult.rows)
            {
                builder.Append("</tr>");
                builder.Append("<tr>");
                builder.Append("<td>" + item.cell[0] + "</td>");
                builder.Append("<td>" + item.cell[2] + "</td>");
                builder.Append("<td>" + item.cell[3] + "</td>");
                builder.Append("<td>" + item.cell[4] + "</td>");
                builder.Append("<td>" + item.cell[5] + "</td>");
            }
            builder.Append("</table>");

            HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
            result.Content = new StringContent(builder.ToString());
            result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");                                
            result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            result.Content.Headers.ContentDisposition.FileName = "file.xls";

            return result;
        }
        else
        {
            throw ForbiddenResponseMessage();
        }

    }

This is the header which should also return the file: https://skydrive.live.com/redir?resid=2D85E5C937AC2BF9!77093

What I want is to have the file downloaded when I call the url which points to the excel method. I do not understand why it won't download. Is it even possible to download a file this way?


回答1:


Alright I figured it out. To spare you the reading, it is not possible to do it the way I want.

First, I was not able to use jQuery Ajax to download the file. As I already expected (or feared) it is not possible to download a file using Ajax. This is due to security concerns. See Why threre is no way to download file using ajax request?

But I still need to download the file. With the above knowledge the solution/workaround was seemed simple.

I changed the javascript function which did the ajax call to the following:

    var gridInfo = {
    sidx: "",
    sord: "asc",
    nd: 1371046879480,
    rows: 50,
    page: 1
}
var payLoad = "?" + PrepareSearchQueryString() + "&" + serialize(gridInfo);

window.location= "http://site:59390/api/mycontroller/excel" + payLoad

But this only covers part of the problem since I need to set the authorization header.

Thanks to Stackoverflow user SLaks who answered my related question (Set headers using javascript) I was able to find out that url with username and password looking like https://user:password@domain.com/path?query could be a solution. Sadly, it did not work. It wasn't accepted by IE and in Chrome using this there was no Authorization Header to be found.

Since I am also using jqGrid I was able to set the authorization head using jqGrid. This works perfectly fine when using the Search function on the site. My Javascript Export function now looks like this:

    var sUrl = "http://localhost:59390/api/Wmssettings/excel" + "?" + PrepareSearchQueryString() ;
$("#gridOverview").jqGrid('excelExport', { url: sUrl });

But looking at the request I noticed that unlike when using the Search function there are no authorization headers passed. I did find out the reason. When looking at the jqGrid source I noticed that the Grid is just doing a window.location to point to the download location. And when doing that there is no way to pass the basic auth information.

So the only way for me to go, is the way I was trying to avoid. I changed my controller method to return a json containing a url pointing to the file and then I use javascript to do the redirecting to a new controller named downloadcontroller.

Controller Excel Method

        [HttpGet]
    // Get api/wmssettings/excel 
    public HttpResponseMessage Excel(string sidx, string sord, int page, int rows, string Depot, string PDID, string User, string Property, string Value)
    {
        if (AuthHelper.AuthService.HasValidCredentials(Request))
        {
            var gridResult = this.GetDataJqGridFormat(sidx, sord, page, rows, Depot, PDID, User, Property, Value);

            // Generate a HTML table.
            StringBuilder builder = new StringBuilder();
            // We create a html table:
            builder.Append("<table border=1>");
            builder.Append("<tr><td>DEPOT</td>");
            builder.Append("<td>PDID</td>");
            builder.Append("<td>USER</td>");
            builder.Append("<td>PROPERTY</td>");
            builder.Append("<td>VALUE</td></tr>");
            // Create response from anonymous type            
            foreach (var item in gridResult.rows)
            {
                builder.Append("</tr>");
                builder.Append("<tr>");
                builder.Append("<td>" + item.cell[0] + "</td>");
                builder.Append("<td>" + item.cell[2] + "</td>");
                builder.Append("<td>" + item.cell[3] + "</td>");
                builder.Append("<td>" + item.cell[4] + "</td>");
                builder.Append("<td>" + item.cell[5] + "</td>");
            }
            builder.Append("</table>");

            // Put all in a file and return the url:
            string fileName = "export" + "_" + Guid.NewGuid().ToString() + ".xls";
            using (StreamWriter writer = new StreamWriter(HttpContext.Current.Server.MapPath("~/Downloads" + fileName)))
            {
                writer.Write(builder.ToString());
                writer.Flush();                    
            }
            HttpResponseMessage result = Request.CreateResponse(HttpStatusCode.OK, fileName);
            return result;
        }
        else
        {
            throw ForbiddenResponseMessage();
        }
    }

JavaScript Export Method

    var gridParams = {
    //"nd": Math.floor((Math.random() * 10000000) + 1),
    sidx: "",
    sord: "asc",
    page: "1",
    rows: "50"        
}   

payLoad = PrepareSearchQueryString() + "&" + serialize(gridParams);

var excelRequest = $.ajax({
    url: 'http://localhost:59390/api/Wmssettings/excel',
    cache: false,
    type: 'GET',
    data: payLoad,
    dataType: 'json',
    contentType: 'application/json; charset=utf-8'
});

excelRequest.success(function (data, code, jqXHR) {
    if (data == null) {
        alert('sd');
        ShowErrorMessage("There was no file created.", "", "Title");
    } else {
        window.location = 'http://localhost:59390/api/download/?fileName=' + data;
    }
});

I added the following line in my WebApiConfig.cs

            config.Routes.MapHttpRoute("Downloadcontroller", "api/{controller}/{action}", 
             new { action = "Get"}, new { httpMethod = new HttpMethodConstraint(allowedVerbsGet), controller="download"});    

And finally the download controller:

    public class DownloadController : ApiController
{
    [HttpGet]
    public HttpResponseMessage Get(string fileName)
    {
        HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
        string fileLocation = HttpContext.Current.Server.MapPath("~/Downloads" + fileName);

        if (!File.Exists(fileLocation))
        {
            throw new HttpResponseException(HttpStatusCode.OK);
        }

        Stream fileStream = File.Open(fileLocation, FileMode.Open);
        result.Content = new StreamContent(fileStream);

        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.ms-excel");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
        return result;
    }
}

The following things can be concluded:

  • Downloading a file using Ajax is not possible, instead I have to redirect to or the precise file location or the another controller.

  • Putting the username and password in the url is not allowed in IE and seems to give problems in other browsers. For example, in Chrome the authorization header stays empty.

  • When doing a window.location there is no way to pass additional headers like the authorization header needed for basic auth.

Well that's it. The answer is that the things I wanted to do are not possible in the way I preferred it. It works fine now.



来源:https://stackoverflow.com/questions/17084231/web-api-wont-download-file-using-jquery-ajax-and-basic-auth

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!