781
技術社區[雲棲]
通過擴展讓ASP.NET Web API支持JSONP
同源策略(Same Origin Policy)的存在導致了“源”自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回調函數中。
1: public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
2: {
3: public string Callback { get; private set; }
4:
5: public JsonpMediaTypeFormatter(string callback = null)
6: {
7: this.Callback = callback;
8: }
9:
10: public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
11: {
12: if (string.IsNullOrEmpty(this.Callback))
13: {
14: return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
15: }
16: try
17: {
18: this.WriteToStream(type, value, writeStream, content);
19: return Task.FromResult<AsyncVoid>(new AsyncVoid());
20: }
21: catch (Exception exception)
22: {
23: TaskCompletionSource<AsyncVoid> source = new TaskCompletionSource<AsyncVoid>();
24: source.SetException(exception);
25: return source.Task;
26: }
27: }
28:
29: private void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
30: {
31: JsonSerializer serializer = JsonSerializer.Create(this.SerializerSettings);
32: using(StreamWriter streamWriter = new StreamWriter(writeStream, this.SupportedEncodings.First()))
33: using (JsonTextWriter jsonTextWriter = new JsonTextWriter(streamWriter) { CloseOutput = false })
35: {
36: jsonTextWriter.WriteRaw(this.Callback + "(");
37: serializer.Serialize(jsonTextWriter, value);
38: jsonTextWriter.WriteRaw(")");
39: }
40: }
41:
42: public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
43: {
44: if (request.Method != HttpMethod.Get)
45: {
46: return this;
47: }
48: string callback;
49: if (request.GetQueryNameValuePairs().ToDictionary(pair => pair.Key,
50: pair => pair.Value).TryGetValue("callback", out callback))
51: {
52: return new JsonpMediaTypeFormatter(callback);
53: }
54: return this;
55: }
56:
57: [StructLayout(LayoutKind.Sequential, Size = 1)]
58: private struct AsyncVoid
59: {}
60: }
我們重寫了GetPerRequestFormatterInstance方法,在默認情況下,當ASP.NET Web API采用內容協商機製選擇出與當前請求相匹配的MediaTypeFormatter後,會調用此方法來創建真正用於序列化響應結果的MediaTypeFormatter對象。在重寫的這個GetPerRequestFormatterInstance方法中,我們嚐試從請求的URL中得到攜帶的JavaScript回調函數名稱,即一個名為“callback”的查詢字符串。如果回調函數名不存在,則直接返回自身,否則返回據此創建的JsonpMediaTypeFormatter對象。
二、將JsonpMediaTypeFormatter的應用到ASP.NET Web API中
接下來我們通過於一個簡單的實例來演示同源策略針對跨域Ajax請求的限製。如圖右圖所示,我們利用Visual Studio在同一個解決方案中創建了兩個Web應用。從項目名稱可以看出,WebApi和MvcApp分別為ASP.NET Web API和MVC應用,後者是Web API的調用者。我們直接采用默認的IIS Express作為兩個應用的宿主,並且固定了端口號:WebApi和MvcApp的端口號分別為“3721”和“9527”,所以指向兩個應用的URI肯定不可能是同源的。
我們在WebApi應用中定義了如下一個繼承自ApiController的ContactsController類型,它具有的唯一Action方法GetAllContacts返回一組聯係人列表。
1: public class ContactsController : ApiController
2: {
3: public IEnumerable<Contact> GetAllContacts()
4: {
5: Contact[] contacts = new Contact[]
6: {
7: new Contact{ Name="張三", PhoneNo="123", EmailAddress="zhangsan@gmail.com"},
8: new Contact{ Name="李四", PhoneNo="456", EmailAddress="lisi@gmail.com"},
9: new Contact{ Name="王五", PhoneNo="789", EmailAddress="wangwu@gmail.com"},
10: };
11: return contacts;
12: }
13: }
14:
15: public class Contact
16: {
17: public string Name { get; set; }
18: public string PhoneNo { get; set; }
19: public string EmailAddress { get; set; }
20: }
現在我們在WebApi應用的Global.asax中利用如下的程序創建這個JsonpMediaTypeFormatter對象並添加當前注冊的MediaTypeFormatter列表中。為了讓它被優先選擇,我們將這個JsonpMediaTypeFormatter對象放在此列表的最前端。
1: public class WebApiApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter ());
6: //其他操作
7: }
8: }
接下來們在MvcApp應用中定義如下一個HomeController,默認的Action方法Index會將對應的View呈現出來。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: return View();
6: }
7: }
如下所示的是Action方法Index對應View的定義。我們的目的在於:當頁麵成功加載之後以Ajax請求的形式調用上麵定義的Web API獲取聯係人列表,並將自呈現在頁麵上。如下麵的代碼片斷所示,我們直接調用$.ajax方法並將dataType參數設置為“jsonp”。
1: <html>
2: <head>
3: <title>聯係人列表</title>
4: <script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
5: </head>
6: <body>
7: <ul id="contacts"></ul>
8: <script type="text/javascript">
9: $(function ()
10: {
11: $.ajax({
12: Type : "GET",
13: url : "https://localhost:3721/api/contacts",
14: dataType : "jsonp",
15: success : listContacts
16: });
17: });
18:
19: function listContacts(contacts) {
20: $.each(contacts, function (index, contact) {
21: var html = "<li><ul>";
22: html += "<li>Name: " + contact.Name + "</li>";
23: html += "<li>Phone No:" + contact.PhoneNo + "</li>";
24: html += "<li>Email Address: " + contact.EmailAddress + "</li>";
25: html += "</ul>";
26: $("#contacts").append($(html));
27: });
28: }
29: </script>
30: </body>
31: </html>
直接運行該ASP.NET MVC程序之後,會得到如下圖所示的輸出結果,通過跨域調用Web API獲得的聯係人列表正常地顯示出來。
三、針對JSONP的請求和響應
如下所示的針對JSONP的Ajax請求和響應內容。可以看到請求的URL中通過查詢字符串“callback”提供了JavaScript回調函數的名稱,而響應的主體部分不是單純的JSON對象,而是將JSON對象填充到回調返回中而生成的一個函數調用語句。
1: GET https://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: https://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":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}])
CORS係列文章
[1] 同源策略與JSONP
[2] 利用擴展讓ASP.NET Web API支持JSONP
[3] W3C的CORS規範
[4] 利用擴展讓ASP.NET Web API支持CORS
[5] ASP.NET Web API自身對CORS的支持: 從實例開始
[6] ASP.NET Web API自身對CORS的支持: CORS授權策略的定義和提供
[7] ASP.NET Web API自身對CORS的支持: CORS授權檢驗的實施
[8] ASP.NET Web API自身對CORS的支持: CorsMessageHandler
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 15:33:47