通过扩展让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        : "http://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 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":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}])

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏软件开发

前端MVC Vue2学习总结(七)——ES6与Module模块化、Vue-cli脚手架搭建、开发、发布项目与综合示例

使用vue-cli可以规范项目,提高开发效率,但是使用vue-cli时需要一些ECMAScript6的知识,特别是ES6中的模块管理内容,本章先介绍ES6中的基...

18960
来自专栏cnblogs

knockoutjs 上自己实现的flux

在knockoutjs 上实现 Flux 单向数据流 状态机,主要解决多个组件之间对数据的耦合问题。 一、其实简单 flux的设计理念和实现方案,很大程度上人借...

25080
来自专栏腾讯云Elasticsearch Service

Elasitcsearch 底层系列 Lucene 内核解析之 Stored Fields

Lucene 的 stored fields 主要用于行存文档需要保存的字段内容,每个文档的所有 stored fields 保存在一起,在查询请求需要返回字段...

26110
来自专栏自由而无用的灵魂的碎碎念

Tips in Visual Studio 2008

.NET几乎程序员都在使用visual studio 2008进行开发。可是,你通过它达到最大的开发效率了吗?

13620
来自专栏blackpiglet

如何使用 Gin 和 Gorm 搭建一个简单的 API 服务 (三)

  这是系列文章的第三篇。下面是另外两篇的链接: 如何使用 Gin 和 Gorm 搭建一个简单的 API 服务(一) 如何使用 Gin 和 Gorm 搭建一...

13920
来自专栏林德熙的博客

win10 uwp 验证输入 自定义用户控件 Nuget使用库判断输入字符长度是否要检查长度判断如何写检查用户控件

TextBox是给用户输入,我们有时要用户只输入数字,而用户输入汉字,我们就有提示用户,那么这东西用到次数很多,我们需要做成一个控件。 我们可以用别人的库,我找...

13630
来自专栏软件开发

前端MVC Vue2学习总结(七)——ES6与Module模块化、Vue-cli脚手架搭建、开发、发布项目与综合示例

使用vue-cli可以规范项目,提高开发效率,但是使用vue-cli时需要一些ECMAScript6的知识,特别是ES6中的模块管理内容,本章先介绍ES6中的基...

47870
来自专栏有趣的django

CRM客户关系管理系统(十三) 第十三章、用户自定义认证第十四章、万能通用权限框架设计

46800
来自专栏技术博客

Knockout.Js官网学习(加载或保存JSON数据)

Knockout可以实现很复杂的客户端交互,但是几乎所有的web应用程序都要和服务器端交换数据(至少为了本地存储需要序列化数据),交换数据最方便的就是使用JSO...

16320
来自专栏JackieZheng

探秘Tomcat——一个简易的Servlet容器

即便再简陋的服务器也是服务器,今天就来循着书本的第二章来看看如何实现一个servlet容器。 背景知识   既然说到servlet容器这个名词,我们首先要了解它...

24750

扫码关注云+社区

领取腾讯云代金券