[CORS:跨域資源共享] 通過擴展讓ASP.NET Web API支持JSONP

同源策略(Same OriginPolicy)的存在導致了“源”自A的腳本只能操作“同源”頁面的DOM,“跨源”操作來源於B的頁面將會被拒絕。同源策略以及跨域資源共享在大部分情況下針對的是Ajax請求。同源策略主要限制了通過XMLHttpRequest實現的Ajax請求,如果請求的是一個“異源”地址,瀏覽器將不允許讀取返回的內容。JSONP是一種常用的解決跨域資源共享的解決方案,現在我們利用ASP.NET Web API自身的擴展性提供一種“通用”的JSONP實現方案。


一、JsonpMediaTypeFormatter

在《[CORS:跨域資源共享]
同源策略與JSONP》,我們是在具體的Action方法中將返回的JSON對象“填充”到JavaScript回調函數中,現在我們通過自定義的MediaTypeFormatter來爲JSONP提供一種更爲通用的實現方式。

我們通過繼承 JsonMediaTypeFormatter 定義瞭如下一個 JsonpMediaTypeFormatter類型。它的只讀屬性Callback代表JavaScript回調函數名稱,改屬性在構造函數中指定。在重寫的方法WriteToStreamAsync中,對於非JSONP調用(回調函數不存在),我們直接調用基類的同名方法對響應對象實施針對JSON的序列化,否則調用WriteToStream方法將對象序列化後的JSON字符串填充到JavaScript回調函數中。


using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Web;

namespace WebApi.Util
{
    public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
    {
        public string Callback { get; private set; }

        public JsonpMediaTypeFormatter(string callback = null)
        {
            this.Callback = callback;
        }

        /// <summary>
        /// 在序列化期間調用,用於將指定類型的對象寫入指定流中。
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        /// <param name="writeStream"></param>
        /// <param name="content"></param>
        /// <param name="transportContext"></param>
        /// <returns></returns>
        public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
        {
            if (string.IsNullOrEmpty(this.Callback))
            {
                return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
            }
            try
            {
                this.WriteToStream(type, value, writeStream, content);
                return Task.FromResult<AsyncVoid>(new AsyncVoid());
            }
            catch (Exception exception)
            {
                TaskCompletionSource<AsyncVoid> source = new TaskCompletionSource<AsyncVoid>();
                source.SetException(exception);
                return source.Task;
            }
        }

        /// <summary>
        /// 用於將指定類型的對象寫入指定流中
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        /// <param name="writeStream"></param>
        /// <param name="content"></param>
        private void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
        {
            JsonSerializer serializer = JsonSerializer.Create(this.SerializerSettings);
            using (StreamWriter streamWriter = new StreamWriter(writeStream, this.SupportedEncodings.First()))
            using (JsonTextWriter jsonTextWriter = new JsonTextWriter(streamWriter) { CloseOutput = false })
            {
                jsonTextWriter.WriteRaw(this.Callback + "(");
                serializer.Serialize(jsonTextWriter, value);
                jsonTextWriter.WriteRaw(")");
            }
        }

        /// <summary>
        ///  返回可以爲給定參數設置響應格式的 System.Net.Http.Formatting.MediaTypeFormatter 專用實例。
        /// </summary>
        /// <param name="type"></param>
        /// <param name="request"></param>
        /// <param name="mediaType"></param>
        /// <returns></returns>
        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
        {
            if (request.Method != HttpMethod.Get)
            {
                return this;
            }
            string callback;
            if (request.GetQueryNameValuePairs().ToDictionary(pair => pair.Key,
                 pair => pair.Value).TryGetValue("callback", out callback))
            {
                return new JsonpMediaTypeFormatter(callback);
            }
            return this;
        }

        [StructLayout(LayoutKind.Sequential, Size = 1)]
        private struct AsyncVoid
        { }
    }

}

我們重寫了GetPerRequestFormatterInstance方法,在默認情況下,當ASP.NET WebAPI採用內容協商機制選擇出與當前請求相匹配的MediaTypeFormatter後,會調用此方法來創建真正用於序列化響應結果的MediaTypeFormatter對象。在重寫的這個GetPerRequestFormatterInstance方法中,我們嘗試從請求的URL中得到攜帶的JavaScript回調函數名稱,即一個名爲“callback”的查詢字符串。如果回調函數名不存在,則直接返回自身,否則返回據此創建的JsonpMediaTypeFormatter對象。


二、將JsonpMediaTypeFormatter的應用到ASP.NET Web API中

接下來我們通過於一個簡單的實例來演示同源策略針對跨域Ajax請求的限制。如圖右圖所示,我們利用VisualStudio在同一個解決方案中創建了兩個Web應用。從項目名稱可以看出,WebApi和MvcApp分別爲ASP.NET Web API和MVC應用,後者是Web API的調用者。我們直接採用默認的IIS Express作爲兩個應用的宿主,並且固定了端口號:WebApi和MvcApp的端口號分別爲“3721”和“9527”,所以指向兩個應用的URI肯定不可能是同源的。

我們在WebApi應用中定義瞭如下一個繼承自ApiController的ContactsController類型,它具有的唯一Action方法GetAllContacts返回一組聯繫人列表。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http;
using Newtonsoft;
using Newtonsoft.Json;

namespace WebApi.Controllers
{
    public class ContactsController : ApiController
    {
        public IEnumerable<Contact> GetAllContacts()
        {
            Contact[] contacts = new Contact[]
            {
                new Contact{ Name="張三", PhoneNo="123", EmailAddress="[email protected]"},
                new Contact{ Name="李四", PhoneNo="456", EmailAddress="[email protected]"},
                new Contact{ Name="王五", PhoneNo="789", EmailAddress="[email protected]"},
            };
            return contacts;
        }
    }

    public class Contact
    {
        public string Name { get; set; }
        public string PhoneNo { get; set; }
        public string EmailAddress { get; set; }
    }
}

現在我們在WebApi應用的Global.asax中利用如下的程序創建這個JsonpMediaTypeFormatter對象並添加當前註冊的MediaTypeFormatter列表中。爲了讓它被優先選擇,我們將這個JsonpMediaTypeFormatter對象放在此列表的最前端。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using WebApi.Util;

namespace WebApi
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
             GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter ());
            //其他操作

            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}

接下來們在MvcApp應用中定義如下一個HomeController,默認的Action方法Index會將對應的View呈現出來。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApp.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }
    }
}

如下所示的是Action方法Index對應View的定義。我們的目的在於:當頁面成功加載之後以Ajax請求的形式調用上面定義的Web API獲取聯繫人列表,並將自呈現在頁面上。如下面的代碼片斷所示,我們直接調用$.ajax方法並將dataType參數設置爲“jsonp”。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script src="Scripts/jquery-1.10.2.min.js"></script>
</head>

<body>
    <ul id="contacts"></ul>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                Type: "GET",
                url: "http://localhost:3721/api/contacts",
                dataType: "jsonp",
                success: listContacts
            });
        });

        function listContacts(contacts) {
            $.each(contacts, function (index, contact) {
                var html = "<li><ul>";
                html += "<li>Name: " + contact.Name + "</li>";
                html += "<li>Phone No:" + contact.PhoneNo + "</li>";
                html += "<li>Email Address: " + contact.EmailAddress + "</li>";
                html += "</ul>";
                $("#contacts").append($(html));
            });
        }
    </script>

</body>

</html>

直接運行該ASP.NET MVC程序之後,會得到如下圖所示的輸出結果,通過跨域調用Web API獲得的聯繫人列表正常地顯示出來。

這裏寫圖片描述


三、針對JSONP的請求和響應

如下所示的針對JSONP的Ajax請求和響應內容。可以看到請求的URL中通過查詢字符串“callback”提供了JavaScript回調函數的名稱,而響應的主體部分不是單純的JSON對象,而是將JSON對象填充到回調返回中而生成的一個函數調用語句。

這裏寫圖片描述


1: GET http://localhost:3721/api/contacts?callback=jQuery110205729522893670946_1386232694513 &_=1386232694514 HTTP/1.1
2: Host: localhost:3721
3: Connection: keep-alive
4: Accept: */*
5: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
6: Referer: http://localhost:9527/
7: Accept-Encoding: gzip,deflate,sdch
8:  
9: HTTP/1.1 200 OK
10: Cache-Control: no-cache
11: Pragma: no-cache
12: Content-Type: application/json; charset=utf-8
13: Expires: -1
14: Server: Microsoft-IIS/8.0
15: X-AspNet-Version: 4.0.30319
16: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29ud?=
17: X-Powered-By: ASP.NET
18: Date: Thu, 05 Dec 2013 08:38:15 GMT
19: Content-Length: 248
20:  
21: jQuery110205729522893670946_1386232694513([{"Name":"三","PhoneNo":"123","EmailAddress":"[email protected]"},{"Name":"四","PhoneNo":"456","EmailAddress":"[email protected]"},{"Name":"王五","PhoneNo":"789","EmailAddress":[email protected]}])

本文轉載:http://www.cnblogs.com/artech/p/cors-4-asp-net-web-api-03.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章