您的位置:首页 > 编程语言 > ASP

通过扩展让ASP.NET Web API支持W3C的CORS规范

2013-12-09 11:30 387 查看

通过扩展让ASP.NET Web API支持W3C的CORS规范

[code] [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]

public class CorsAttribute: Attribute

{

 public Uri[]AllowOrigins{ get; private set;}

 public string ErrorMessage{ get; private set;}


 public CorsAttribute(params string[] allowOrigins)

{

 this.AllowOrigins = (allowOrigins ?? new string[0]).Select(origin => new Uri(origin)).ToArray();

}


 public bool TryEvaluate(HttpRequestMessage request, out IDictionary<string, string> headers)

{

 headers = null;

 string origin = request.Headers.GetValues("Origin").First();

 Uri originUri = new Uri(origin);

 if (this.AllowOrigins.Contains(originUri))

{

 headers = this.GenerateResponseHeaders(request);

 return true;

}

 this.ErrorMessage = "Cross-origin request denied";

 return false;

}


 private IDictionary<string, string> GenerateResponseHeaders(HttpRequestMessage request)

{

 //设置响应报头"Access-Control-Allow-Methods"

 string origin = request.Headers.GetValues("Origin").First();

 Dictionary<string, string> headers = new Dictionary<string, string>();

 headers.Add("Access-Control-Allow-Origin", origin);

 if (request.IsPreflightRequest())

{

 //设置响应报头"Access-Control-Request-Headers"

 //和"Access-Control-Allow-Headers"

 headers.Add("Access-Control-Allow-Methods", "*");

 string requestHeaders = request.Headers.GetValues("Access-Control-Request-Headers").FirstOrDefault();

 if (!string.IsNullOrEmpty(requestHeaders))

{

 headers.Add("Access-Control-Allow-Headers", requestHeaders);

}

}

 return headers;

}

}

[/code]
[/code]
我们将针对请求的资源授权检查定义在TryEvaluate方法中,其返回至表示请求是否通过了授权检查,输出参数headers通过返回的字典对象表示最终添加的CORS响应报头。在该方法中,我们从指定的HttpRequestMessage对象中提取表示请求站点的“Origin”报头值。如果请求站点没有在通过AllowOrigins属性表示的授权站点内,则意味着请求没有通过授权检查,在此情况下我们会将ErrorMessage属性设置为“Cross-origin request denied”。

在请求成功通过授权检查的情况下,我们调用另一个方法GenerateResponseHeaders根据请求生成我们需要的CORS响应报头。如果当前为简单跨域资源请求,只会返回针对“Access-Control-Allow-Origin”的响应报头,其值为请求站点。对于预检请求来说,我们还需要额外添加针对“Access-Control-Request-Headers”和“Access-Control-Allow-Methods”的响应报头。对于前者,我们直接采用请求的“Access-Control-Request-Headers”报头值,而后者被直接设置为“*”。

在上面的程序中,我们通过调用HttpRequestMessage的扩展方法IsPreflightRequest来判断是否是一个预检请求,该方法定义如下。从给出的代码片断可以看出,我们判断预检请求的条件是:包含报头“Origin”和“Access-Control-Request-Method”的HTTP-OPTIONS请求。

[code]
[code] public static class HttpRequestMessageExtensions

{

 public static bool IsPreflightRequest(this HttpRequestMessage request)

{

 return request.Method == HttpMethod.Options &&

 request.Headers.GetValues("Origin").Any() &&

 request.Headers.GetValues("Access-Control-Request-Method").Any();

}

}

[/code]
[/code]

三、实施CORS授权检验的HttpMessageHandler——CorsMessageHandler

针对跨域资源共享的实现最终体现在具有如下定义的CorsMessageHandler类型上,它直接继承自DelegatingHandler。在实现的SendAsync方法中,CorsMessageHandler利用应用在目标Action方法或者HttpController类型上CorsAttribute来对请求实施授权检验,最终将生成的CORS报头添加到响应报头列表中。

[code]
[code] public class CorsMessageHandler: DelegatingHandler

{

 protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

{

 //得到描述目标Action的HttpActionDescriptor

 HttpMethod originalMethod = request.Method;

 bool isPreflightRequest = request.IsPreflightRequest();

 if (isPreflightRequest)

{

 string method = request.Headers.GetValues("Access-Control-Request-Method").First();

 request.Method = new HttpMethod(method);

}

 HttpConfiguration configuration = request.GetConfiguration();

 HttpControllerDescriptor controllerDescriptor = configuration.Services.GetHttpControllerSelector().SelectController(request);

 HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), request.GetRouteData(), request)

{

 ControllerDescriptor = controllerDescriptor

};

 HttpActionDescriptor actionDescriptor = configuration.Services.GetActionSelector().SelectAction(controllerContext);


 //根据HttpActionDescriptor得到应用的CorsAttribute特性

 CorsAttribute corsAttribute = actionDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault()??

 controllerDescriptor.GetCustomAttributes<CorsAttribute>().FirstOrDefault();

 if(null == corsAttribute)

{

 return base.SendAsync(request, cancellationToken);

}


 //利用CorsAttribute实施授权并生成响应报头

 IDictionary<string,string> headers;

 request.Method = originalMethod;

 bool authorized = corsAttribute.TryEvaluate(request, out headers);

 HttpResponseMessage response;

 if (isPreflightRequest)

{

 if (authorized)

{

 response = new HttpResponseMessage(HttpStatusCode.OK); 

}

 else

{

 response = request.CreateErrorResponse(HttpStatusCode.BadRequest, corsAttribute.ErrorMessage);

}

}

 else

{

 response = base.SendAsync(request, cancellationToken).Result;

}


 //添加响应报头

 foreach (var item in headers)

{

 response.Headers.Add(item.Key, item.Value);

}

 return Task.FromResult<HttpResponseMessage>(response);

}

}

[/code]
[/code]
具体来说,我们通过注册到当前ServicesContainer上的HttpActionSelector根据请求得到描述目标Action的HttpActionDescriptor对象,为此我们需要根据请求手工生成作为HttpActionSelector的SelectAction方法参数的HttpControllerContext对象。对此有一点需要注意:由于预检请求采用的HTTP方法为“OPTIONS”,我们需要将其替换成代表真正跨域资源请求的HTTP方法,也就是预检请求的“Access-Control-Request-Method”报头值。

在得到描述目标Action的HttpActionDescriptor对象后,我们调用其GetCustomAttributes<T>方法得到应用在Action方法上的CorsAttribute特性。如果这样的特性不存在,在调用同名方法得到应用在HttpController类型上的CorsAttribute特性。

接下来我们调用CorsAttribute的TryEvaluate方法对请求实施资源授权检查并得到一组CORS响应报头,作为参数的HttpRequestMessage对象的HTTP方法应该恢复其原有的值。对于预检请求,在请求通过授权检查之后我们会创建一个状态为“200, OK”的响应,否则会根据错误消息创建创建一个状态为“400, Bad Request”的响应。

对于非预检请求来说(可能是简单跨域资源请求,也可能是继预检请求之后发送的真正的跨域资源请求),我们调用基类的SendAsync方法将请求交付给后续的HttpMessageHandler进行处理并最终得到最终的响应。我们最终将调用CorsAttribute的TryEvaluate方法得到的响应报头逐一添加到响应报头列表中。

四、CorsMessageHandler针对简单跨域资源请求的授权检验

[code] public class ContactsController : ApiController

{

 [Cors("http://localhost:9527")] 

 public IHttpActionResult GetAllContacts()

{

 Contact[] contacts = new Contact[]

{

 new Contact{ Name="张三", PhoneNo="123", EmailAddress="zhangsan@gmail.com"},

 new Contact{ Name="李四", PhoneNo="456", EmailAddress="lisi@gmail.com"},

 new Contact{ Name="王五", PhoneNo="789", EmailAddress="wangwu@gmail.com"},

};

 return Json<IEnumerable<Contact>>(contacts);

}

}

[/code]
[/code]
在Global.asax中,我们采用如下的方式将一个CorsMessageHandler对象添加到ASP.NET Web API的消息处理管道中。

[code]
[code] public class WebApiApplication : System.Web.HttpApplication

{

 protected void Application_Start()

{

 GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsMessageHandler ());

 //其他操作

}

}

[/code]
[/code]
接下来们在MvcApp应用中定义如下一个HomeController,默认的Action方法Index会将对应的View呈现出来。

[code]
[code] public class HomeController : Controller

{

 public ActionResult Index()

{

 return View();

}

}

[/code]
[/code]
如下所示的是Action方法Index对应View的定义。我们的目的在于:当页面成功加载之后以Ajax请求的形式调用上面定义的Web API获取联系人列表,并将自呈现在页面上。如下面的代码片断所示,Ajax调用和返回数据的呈现是通过调用jQuery的getJSON方法完成的。在此基础上直接调用我们的ASP.NET MVC程序照样会得到如右图所示的结果.

[code]
[code] <html>

<head>

 <title>联系人列表</title>

<script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>


</head>

<body>

 <ul id="contacts"></ul>

 <script type="text/javascript">

 $(function ()

{

 var url = "http://localhost:3721/api/contacts";

 $.getJSON(url, null, function (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>

[/code]
[/code]
如果我们利用Fiddler来检测针对Web API调用的Ajax请求,如下所示的请求和响应内容会被捕捉到,我们可以清楚地看到利用CorsMessageHandler添加的“Access-Control-Allow-Origin”报头出现在响应的报头集合中。

[code]
[code]GET http://localhost:3721/api/contacts HTTP/1.1

Host: localhost:3721

Connection: keep-alive

Accept: application/json, text/javascript, */*; q=0.01

Origin: http://localhost:9527[/code] 
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36

Referer: http://localhost:9527/[/code] 
Accept-Encoding: gzip,deflate,sdch

Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4


HTTP/1.1 200 OK

Cache-Control: no-cache

Pragma: no-cache

Content-Length: 205

Content-Type: application/json; charset=utf-8

Expires: -1

Server: Microsoft-IIS/8.0

Access-Control-Allow-Origin: http://localhost:9527

X-AspNet-Version: 4.0.30319

X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPnmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29udGFjdHM=?=

X-Powered-By: ASP.NET

Date: Wed, 04 Dec 2013 01:50:01 GMT


[{"Name":"张三","PhoneNo":"123","EmailAddress":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}]

[/code]
[/code]

五、CorsMessageHandler针对Preflight Request的授权检验

从上面给出的请求和响应内容可以确定Web API的调用采用的是“简单跨域资源请求”,所以并没有采用“预检”机制。如何需要迫使浏览器采用预检机制,就需要了解我们在《W3C的CORS Specification》上面提到的简单跨域资源请求具有的两个条件

采用简单HTTP方法(GET、HEAD和POST);

不具有非简单请求报头的自定义报头。

只要打破其中任何一个条件就会迫使浏览器采用预检机制,我们选择为请求添加额外的自定义报头。在ASP.NET MVC应用用户调用Web API的View中,针对Ajax请求调用Web API的JavaScript程序被改写成如下的形式:我们在发送Ajax请求之前利用setRequestHeader函数添加了两个名称分别为“'X-Custom-Header1”和“'X-Custom-Header2”的自定义报头。

[code]
[code] <html>

<head>

 <title>联系人列表</title>

<script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>



</head>


<body>


 <ul id="contacts"></ul>


 <script type="text/javascript">


 $(function ()


{


 $.ajax({


 url : 'http://localhost:3721/api/contacts',


 type: 'GET',


 success : listContacts,


 beforeSend: setRequestHeader


});


});




 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));


});


}




 function setRequestHeader(xmlHttpRequest)


{


 xmlHttpRequest.setRequestHeader('X-Custom-Header1', 'Foo');


 xmlHttpRequest.setRequestHeader('X-Custom-Header2', 'Bar');


}


 

</script>

</body>

</html>

[/code]
[/code]
再次运行我们的ASP.NET MVC程序,依然会得正确的输出结果,但是针对Web API的调用则会涉及到两次消息交换,分别针对预检请求和真正的跨域资源请求。从下面给出的两次消息交换涉及到的请求和响应内容可以看出:自定义的两个报头名称会出现在采用“OPTIONS”作为HTTP方法的预检请求的“Access-Control-Request-Headers”报头中,利用CorsMessageHandler添加的3个报头(“Access-Control-Allow-Origin”、“Access-Control-Allow-Methods”和“Access-Control-Allow-Headers”)均出现在针对预检请求的响应中。

[code]
[code] OPTIONS http://localhost:3721/api/contacts HTTP/1.1

Host: localhost:3721

Connection: keep-alive

Cache-Control: max-age=0

Access-Control-Request-Method: GET

Origin: http://localhost:9527[/code] 
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36

Access-Control-Request-Headers: accept, x-custom-header1, x-custom-header2

Accept: */*

Referer: http://localhost:9527/[/code] 
Accept-Encoding: gzip,deflate,sdch

Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4


HTTP/1.1 200 OK

Cache-Control: no-cache

Pragma: no-cache

Expires: -1

Server: Microsoft-IIS/8.0

Access-Control-Allow-Origin: http://localhost:9527

Access-Control-Allow-Methods: *

Access-Control-Allow-Headers: accept, x-custom-header1, x-custom-header2

X-AspNet-Version: 4.0.30319

X-SourceFiles: =?UTF-8?B??=

X-Powered-By: ASP.NET

Date: Wed, 04 Dec 2013 02:11:16 GMT

Content-Length: 0


--------------------------------------------------------------------------------

GET http://localhost:3721/api/contacts HTTP/1.1

Host: localhost:3721

Connection: keep-alive

Accept: */*

X-Custom-Header1: Foo

Origin: http://localhost:9527[/code] 
X-Custom-Header2: Bar

User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36

Referer: http://localhost:9527/[/code] 
Accept-Encoding: gzip,deflate,sdch

Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4


HTTP/1.1 200 OK

Cache-Control: no-cache

Pragma: no-cache

Content-Length: 205

Content-Type: application/json; charset=utf-8

Expires: -1

Server: Microsoft-IIS/8.0

Access-Control-Allow-Origin: http://localhost:9527[/code] 
X-AspNet-Version: 4.0.30319

X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XF9udGFjdHM=?=

X-Powered-By: ASP.NET

Date: Wed, 04 Dec 2013 02:11:16 GMT


[{"Name":"张三","PhoneNo":"123","EmailAddress":"zhangsan@gmail.com"},{"Name":"李四","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"王五","PhoneNo":"789","EmailAddress":wangwu@gmail.com}]

[/code]
[/code]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: