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

讓ASP.NET Web API 支持 JSONP 和 W3C 的 CORS 規範是解決“跨域資源共享”的兩種途徑,在《通過擴展讓ASP.NET Web API支持JSONP》中我們實現了前者,並且在《W3C的CORS Specification》一文中我們對 W3C 的 CORS 規範進行了詳細介紹,現在我們通過一個具體的實例來演示如何利用 ASP.NET Web API 具有的擴展點來實現針對CORS的支持。

目錄

一、ActionFilter OR HttpMessageHandler 
二、用於定義CORS資源授權策略的特性——CorsAttribute 
三、實施CORS授權檢驗的HttpMessageHandler——CorsMessageHandler 
四、CorsMessageHandler針對簡單跨域資源請求的授權檢驗 
五、CorsMessageHandler針對Preflight Request的授權檢驗

一、ActionFilter OR HttpMessageHandler

通過上面針對 W3C 的 CORS 規範的介紹,我們知道跨域資源共享實現的途徑就是資源的提供者利用預定義的響應報頭表明自己是否將提供的資源授權給了客戶端 JavaScript 程序,而支持 CORS 的瀏覽器利用這些響應報頭決定是否允許 JavaScrip t程序操作返回的資源。對於ASP .NET Web API 來說,如果我們具有一種機制能夠根據預定義的資源授權規則自動生成和添加針對 CORS 的響應報頭,那麼資源的跨域共享就迎刃而解了。

那麼如何利用 ASP.NET Web API 的擴展實現針對 CORS 響應報頭的自動添加呢?可能有人首先想到的是利用 HttpActionFilter 在目標Action方法執行之後自動添加 CORS 響應報頭。這種解決方案對於簡單跨域資源請求是沒有問題的,但是不要忘了:對於非簡單跨域資源請求,瀏覽器會採用“預檢(Preflight)”機制。目標 Action 方法只會在處理真正跨域資源請求的過程中才會執行,但是對於採用“OPTIONS”作爲HTTP方法的預檢請求,根本找不到匹配的目標 Action 方法。

爲了能夠有效地應付瀏覽器採用的預檢機制,我們只能在ASP.NET Web API的 消息處理管道 級別實現對提供資源的授權檢驗和對 CORS 響應報頭的添加。我們只需要爲此創建一個自定義的 HttpMessageHandler 即可,不過在此之前我們先來介紹用於定義資源授權策略的 CorsAttribute 特性。


二、用於定義CORS資源授權策略的特性——CorsAttribute

我們將具有如下定義的CorsAttribute特性直接應用到某個HttpController或者定義其中的某個Action方法上來定義相關的資源授權策略。簡單起見,我們的授權策略只考慮請求站點,而忽略請求提供的自定義報頭和攜帶的用戶憑證。如下面的代碼片斷所示,CorsAttribute具有一個只讀屬性AllowOrigins表示一組被授權站點對應的Uri數組,具體站點列表在構造函數中指定。另一個只讀屬性ErrorMessage表示在請求沒有通過授權檢驗情況下返回的錯誤消息。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web;

namespace WebApi.Util
{
    /// <summary>
    /// Cors特性
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
    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 = null;
            try
            {
                origin = request.Headers.GetValues("Origin").FirstOrDefault();
            }
            catch (Exception)
            {
                this.ErrorMessage = "Cross-origin request denied";
                return false;
            }
            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;
        }
    }

    /// <summary>
    /// HttpRequestMessage擴展方法
    /// </summary>
    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();
        }
    }

}

我們將針對請求的資源授權檢查定義在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請求。

    /// <summary>
    /// HttpRequestMessage擴展方法
    /// </summary>
    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();
        }
    }

三、實施CORS授權檢驗的HttpMessageHandler——CorsMessageHandler

針對跨域資源共享的實現最終體現在具有如下定義的CorsMessageHandler類型上,它直接繼承自DelegatingHandler。在實現的SendAsync方法中,CorsMessageHandler利用應用在目標Action方法或者HttpController類型上CorsAttribute來對請求實施授權檢驗,最終將生成的CORS報頭添加到響應報頭列表中。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;

namespace WebApi.Util
{
    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);
        }
    }

}

具體來說,我們通過註冊到當前ServicesContainer上的HttpActionSelector根據請求得到描述目標Action的HttpActionDescriptor對象,爲此我們需要根據請求手工生成作爲HttpActionSelector的SelectAction方法參數的HttpControllerContext對象。對此有一點需要注意:由於預檢請求採用的HTTP方法爲“OPTIONS”,我們需要將其替換成代表真正跨域資源請求的HTTP方法,也就是預檢請求的“Access-Control-Request-Method”報頭值。

在得到描述目標Action的HttpActionDescriptor對象後,我們調用其GetCustomAttributes方法得到應用在Action方法上的CorsAttribute特性。如果這樣的特性不存在,在調用同名方法得到應用在HttpController類型上的CorsAttribute特性。

接下來我們調用CorsAttribute的TryEvaluate方法對請求實施資源授權檢查並得到一組CORS響應報頭,作爲參數的HttpRequestMessage對象的HTTP方法應該恢復其原有的值。對於預檢請求,在請求通過授權檢查之後我們會創建一個狀態爲“200, OK”的響應,否則會根據錯誤消息創建創建一個狀態爲“400, Bad Request”的響應。

對於非預檢請求來說(可能是簡單跨域資源請求,也可能是繼預檢請求之後發送的真正的跨域資源請求),我們調用基類的SendAsync方法將請求交付給後續的HttpMessageHandler進行處理並最終得到最終的響應。我們最終將調用CorsAttribute的TryEvaluate方法得到的響應報頭逐一添加到響應報頭列表中。


四、CorsMessageHandler針對簡單跨域資源請求的授權檢驗

這裏寫圖片描述

接下來我們通過於一個簡單的實例來演示同源策略針對跨域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返回一組聯繫人列表。

如下面的代碼片斷所示,用於獲取所有聯繫人列表的Action方法GetAllContacts返回一個Json對象,但是該方法上面應用了我們定義的CorsAttribute特性,並將“http://localhost:9527”(客戶端ASP.NET MVC應用的站點)設置爲允許授權的站點。

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;
using WebApi.Util;

namespace WebApi.Controllers
{
    [Cors("http://localhost:9527")]
    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; }
    }
}

在Global.asax中,我們採用如下的方式將一個CorsMessageHandler對象添加到ASP.NET Web API的消息處理管道中。

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.MessageHandlers.Add(new CorsMessageHandler());

            //其他操作

            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調用和返回數據的呈現是通過調用jQuery的getJSON方法完成的。在此基礎上直接調用我們的ASP.NET MVC程序照樣會得到如右圖所示的結果.

<!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 () {
            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>

這裏寫圖片描述


如果我們利用Fiddler來檢測針對Web API調用的Ajax請求,如下所示的請求和響應內容會被捕捉到,我們可以清楚地看到利用CorsMessageHandler添加的“Access-Control-Allow-Origin”報頭出現在響應的報頭集合中。

    1: GET http://localhost:3721/api/contacts HTTP/1.1
    2: Host: localhost:3721
    3: Connection: keep-alive
    4: Accept: application/json, text/javascript, */*; q=0.01
    5: Origin: http://localhost:9527
    6: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
    7: Referer: http://localhost:9527/
    8: Accept-Encoding: gzip,deflate,sdch
    9: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
   10:  
   11: HTTP/1.1 200 OK
   12: Cache-Control: no-cache
   13: Pragma: no-cache
   14: Content-Length: 205
   15: Content-Type: application/json; charset=utf-8
   16: Expires: -1
   17: Server: Microsoft-IIS/8.0
   18: Access-Control-Allow-Origin: http://localhost:9527 
  19: X-AspNet-Version: 4.0.30319
   20: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPnmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29udGFjdHM=?=
   21: X-Powered-By: ASP.NET
   22: Date: Wed, 04 Dec 2013 01:50:01 GMT
   23:  
   24: [{"Name":"張三","PhoneNo":"123","EmailAddress":"[email protected]"},{"Name":"李四","PhoneNo":"456","EmailAddress":"[email protected]"},{"Name":"王五","PhoneNo":"789","EmailAddress":[email protected]}]

五、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”的自定義報頭。


<!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({
                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>

再次運行我們的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”)均出現在針對預檢請求的響應中。

    1: OPTIONS http://localhost:3721/api/contacts HTTP/1.1
    2: Host: localhost:3721
    3: Connection: keep-alive
    4: Cache-Control: max-age=0
    5: Access-Control-Request-Method: GET 
   6: Origin: http://localhost:9527
    7: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
    8: Access-Control-Request-Headers: accept, x-custom-header1, x-custom-header2 
   9: Accept: */*
   10: Referer: http://localhost:9527/
   11: Accept-Encoding: gzip,deflate,sdch
   12: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
   13:  
   14: HTTP/1.1 200 OK
   15: Cache-Control: no-cache
   16: Pragma: no-cache
   17: Expires: -1
   18: Server: Microsoft-IIS/8.0
   19: Access-Control-Allow-Origin: http://localhost:9527 
  20: Access-Control-Allow-Methods: * 
  21: Access-Control-Allow-Headers: accept, x-custom-header1, x-custom-header2 
  22: X-AspNet-Version: 4.0.30319
   23: X-SourceFiles: =?UTF-8?B??=
   24: X-Powered-By: ASP.NET
   25: Date: Wed, 04 Dec 2013 02:11:16 GMT
   26: Content-Length: 0
   27:  
   28: --------------------------------------------------------------------------------
   29: GET http://localhost:3721/api/contacts HTTP/1.1
   30: Host: localhost:3721
   31: Connection: keep-alive
   32: Accept: */*
   33: X-Custom-Header1: Foo
   34: Origin: http://localhost:9527
   35: X-Custom-Header2: Bar
   36: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
   37: Referer: http://localhost:9527/
   38: Accept-Encoding: gzip,deflate,sdch
   39: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
   40:  
   41: HTTP/1.1 200 OK
   42: Cache-Control: no-cache
   43: Pragma: no-cache
   44: Content-Length: 205
   45: Content-Type: application/json; charset=utf-8
   46: Expires: -1
   47: Server: Microsoft-IIS/8.0
   48: Access-Control-Allow-Origin: http://localhost:9527
   49: X-AspNet-Version: 4.0.30319
   50: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XF9udGFjdHM=?=
   51: X-Powered-By: ASP.NET
   52: Date: Wed, 04 Dec 2013 02:11:16 GMT
   53:  
   54: [{"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-04.html

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