通过扩展让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.36Referer: http://localhost:9527/[/code]Accept-Encoding: gzip,deflate,sdchAccept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4HTTP/1.1 200 OKCache-Control: no-cachePragma: no-cacheContent-Length: 205Content-Type: application/json; charset=utf-8Expires: -1Server: Microsoft-IIS/8.0Access-Control-Allow-Origin: http://localhost:9527X-AspNet-Version: 4.0.30319X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPnmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29udGFjdHM=?=X-Powered-By: ASP.NETDate: 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.1Host: localhost:3721Connection: keep-aliveCache-Control: max-age=0Access-Control-Request-Method: GETOrigin: 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.36Access-Control-Request-Headers: accept, x-custom-header1, x-custom-header2Accept: */*Referer: http://localhost:9527/[/code]Accept-Encoding: gzip,deflate,sdchAccept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4HTTP/1.1 200 OKCache-Control: no-cachePragma: no-cacheExpires: -1Server: Microsoft-IIS/8.0Access-Control-Allow-Origin: http://localhost:9527Access-Control-Allow-Methods: *Access-Control-Allow-Headers: accept, x-custom-header1, x-custom-header2X-AspNet-Version: 4.0.30319X-SourceFiles: =?UTF-8?B??=X-Powered-By: ASP.NETDate: Wed, 04 Dec 2013 02:11:16 GMTContent-Length: 0--------------------------------------------------------------------------------GET http://localhost:3721/api/contacts HTTP/1.1Host: localhost:3721Connection: keep-aliveAccept: */*X-Custom-Header1: FooOrigin: http://localhost:9527[/code]X-Custom-Header2: BarUser-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36Referer: http://localhost:9527/[/code]Accept-Encoding: gzip,deflate,sdchAccept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4HTTP/1.1 200 OKCache-Control: no-cachePragma: no-cacheContent-Length: 205Content-Type: application/json; charset=utf-8Expires: -1Server: Microsoft-IIS/8.0Access-Control-Allow-Origin: http://localhost:9527[/code]X-AspNet-Version: 4.0.30319X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XF9udGFjdHM=?=X-Powered-By: ASP.NETDate: 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]
相关文章推荐
- 通过扩展让ASP.NET Web API支持W3C的CORS规范(转载)
- [CORS:跨域资源共享] 通过扩展让ASP.NET Web API支持W3C的CORS规范
- 通过扩展让ASP.NET Web API支持W3C的CORS规范
- 通过扩展让ASP.NET Web API支持W3C的CORS规范
- [CORS:跨域资源共享] 通过扩展让ASP.NET Web API支持JSONP
- 通过微软的cors类库,让ASP.NET Web API 支持 CORS
- 通过扩展让ASP.NET Web API支持JSONP
- 通过扩展让ASP.NET Web API支持JSONP -摘自网络
- 通过扩展让ASP.NET Web API支持JSONP
- 通过微软的cors类库,让ASP.NET Web API 支持 CORS
- 通过扩展让ASP.NET Web API支持JSONP
- 通过微软的cors类库,让ASP.NET Web API 支持 CORS
- (转)通过扩展让ASP.NET Web API支持JSONP
- 通过扩展让ASP.NET Web API支持JSONP ----- .NET 4.0 asp.net WebApi(不是WebApi 2)
- 通过扩展让ASP.NET Web API支持JSONP
- ASP.NET Web API 2 对 CORS 的支持
- ASP.NET Web API自身对CORS的支持: EnableCorsAttribute特性背后的故事
- ASP.NET Web API自身对CORS的支持:从实例开始
- ASP.NET Web API自身对CORS的支持: CORS授权检验的实施
- 支持Ajax跨域访问ASP.NET Web Api 2(Cors)的示例