[CORS:跨域資源共享] 同源策略與JSONP

Web API普遍採用面向資源的REST架構,將瀏覽器最終執行上下文的JavaScript應用Web API消費者的重要組成部分。“同源策略”限制了JavaScript的跨站點調用,這必然導致Web API不能垮域提供資源。如果Web API僅限於爲“同源客戶端”提供資源,那麼它都對不起自己的名字,因爲Web本身是一個開放的協議。那麼ASP.NET Web API通過怎樣的方式來實現跨域資源共享呢?

同源策略

瀏覽器是訪問Internet的工具,也是客戶端應用的宿主,它爲客戶端應用提供一個寄宿和運行的環境。而這裏所說的應用,基本是指在瀏覽器中執行的客戶端JavaScript程序。雖然是一種解釋性的腳本語言,JavaScript其實是無比強大的,原則上來講它可以做任何事。但是在能夠在JavaScript腳本並不都是值得信賴的,所以瀏覽器必須對JavaScript的執行作相應的限制,這就是我們接下來着重介紹的“同源策略(Same Origin Policy)”。

同源策略是瀏覽器的一項最爲基本同時也是必須遵守的安全策略,毫不誇張地說,瀏覽器的整個安全體系均建立在此之上。同源策略的存在,限制了“源”自A的腳本只能操作“同源”頁面的DOM,“跨源”操作來源於B的頁面將會被拒絕。所謂的“同源”,必須要求相應的URI在如下3個方面均是相同的。術語“源(Origin)”在中文表達中顯得有點突兀,所以在接下來的內容中,我們更多地會採用“站點(Site)”或者“域(Domain)”這樣的說法,在未作特別說明的情況下均與“源”表達相同的意思。

  • 主機名稱(域名/子域名或者IP地址)
  • 端口號
  • 網絡協議(Scheme,分別採用“http”和“https”協議的兩個URI被視爲不同源)

值得一提的是,對於一段JavaScript腳本來說,其“源”與它存儲的地址無關,而取決於腳本被加載的頁面。比如我們在某個頁面中通過如下所示的<script>標籤引用了來源於不同地方(“http://www.artech.com/”和“http://www.jinnan.me/”)的兩個JavaScript腳本,它們均與當前頁面同源。實際上接下來介紹的基於JSONP跨域資源共享就是利用了這個特性。

   1: <script src="http://www.artech.com/scripts/common.js"></script>
   2: <script src="http://www.jinnan.me/scripts/utility.js"></script>

除了<script>標籤,HTML還具有其它一些具有src屬性的標籤(比如<img>、<iframe>和<link>等),它們均具有跨域加載資源的能力,所以同源策略對它們不做限制。對於這些具有src屬性的HTML標籤來說,標籤的每次加載都意味着針對目標地址的一次HTTP-GET請求。

同源策略以及跨域資源共享在大部分情況下針對的是Ajax請求。同源策略主要限制了通過XMLHttpRequest實現的Ajax請求,如果請求的是一個“異源”地址,瀏覽器將不允許讀取返回的內容,我們可以通過一個簡單的實例來演示這一點。

實例演示:跨域調用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返回一組聯繫人列表。由於具體返回的數據類型爲JsonResult<IEnumerable<Contact>>,所以聯繫人 列表以JSON格式被序列化。

   1: public class ContactsController : ApiController
   2: {
   3:     public IHttpActionResult GetAllContacts()
   4:     {
   5:         Contact[] contacts = new Contact[]
   6:         {
   7:             new Contact{ Name="張三", PhoneNo="123", EmailAddress="[email protected]"},
   8:             new Contact{ Name="李四", PhoneNo="456", EmailAddress="[email protected]"},
   9:             new Contact{ Name="王五", PhoneNo="789", EmailAddress="[email protected]"},
  10:         };
  11:         return Json<IEnumerable<Contact>>(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: }

接下來們在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調用和返回數據的呈現是通過調用jQuery的getJSON方法完成的。

   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:             var url = "http://localhost:3721/api/contacts";
  12:             $.getJSON(url, null, function (contacts) {
  13:                 $.each(contacts, function (index, contact)
  14:                 {
  15:                     var html = "<li><ul>";
  16:                     html += "<li>Name: " + contact.Name + "</li>";
  17:                     html += "<li>Phone No:" + contact.PhoneNo + "</li>";
  18:                     html += "<li>Email Address: " + contact.EmailAddress + "</li>";
  19:                     html += "</ul>";
  20:                     $("#contacts").append($(html));
  21:                 });
  22:             });
  23:         }
  24:         );
  25:     </script>
  26: </body>
  27: </html>

如果運行我們的程序,我們將會得到如右圖所示的空白頁面,這就是“同源策略”導致的後果。值得一提的是,我們並不會得到任何的錯誤信息,這是因爲大部分瀏覽器針對同源策略的支持都是隱性和透明的。如果開發人員對此不瞭解的話,根本想不明白錯誤根源何在。

如果我們採用Fiddler來監測頁面加載過程中發送的請求和接收到的響應,我們會發現針對Web API調用的Ajax請求被成功發送,並且以JSON格式表示的聯繫人列表會被成功接收,請求和響應的內容如下所示。這實際上說明支持同源策略的瀏覽器其實並不會阻止跨域請求的發送和響應的接收,它僅僅是阻止程序獲取和操作返回的數據而已。

   1: GET http://localhost:3721/api/contacts HTTP/1.1
   2: Host: localhost:3721
   3: Connection: keep-alive
   4: Cache-Control: max-age=0
   5: Accept: application/json, text/javascript, */*; q=0.01
   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: Referer: http://localhost:9527/
   9: Accept-Encoding: gzip,deflate,sdch
  10: Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh-TW;q=0.4
  11:  
  12:  
  13: HTTP/1.1 200 OK
  14: Cache-Control: no-cache
  15: Pragma: no-cache
  16: Content-Length: 205
  17: Content-Type: application/json; charset=utf-8
  18: Expires: -1
  19: Server: Microsoft-IIS/8.0
  20: X-AspNet-Version: 4.0.30319
  21: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAxXFdlYkFwaVxhcGlcY29udGFjdHM
  22: X-Powered-By: ASP.NET
  23: Date: Mon, 02 Dec 2013 01:47:53 GMT
  24:  
  25: [{"Name":"張三","PhoneNo":"123","EmailAddress":"[email protected]"},{"Name":"李四","PhoneNo":"456","EmailAddress":"[email protected]"},{"Name":"王五","PhoneNo":"789","EmailAddress":"[email protected]"}]

對於上面給出的針對Web API的Ajax請求的內容,我們可以看到它具有一個名爲“Origin”的報頭。該報頭值表示請求頁面的所在的站點(http://localhost:9527),它可以看成是瀏覽器對CORS(Cross-Origin Resource Sharing)規範的支持。

採用JSONP實現跨域資源共享

上面我們已經說過:JavaScript腳本的源決定於其被加載的頁面,而不是其存儲的地址。對於一段通過<script>標籤的src屬性加載的JavaScript腳本,它與當前頁面同源。對於上面我們演示的實例來說,如果我們按照如下的方式來定義View:聯繫人列表的呈現單獨定義在listContacts函數中(參數contacts表示聯繫人列表),並將Web API的地址置於<script>標籤的src屬性中來間接地調用它。

   1: <html>
   2: <head>
   3:     <title>聯繫人列表</title>
   4:     <script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
   1:     <script type="text/javascript">

   2:         function listContacts(contacts)

   3:         {

   4:             $.each(contacts, function (index, contact) {

   5:                 var html = "<li><ul>";

   6:                 html += "<li>Name: " + contact.Name + "</li>";

   7:                 html += "<li>Phone No:" + contact.PhoneNo + "</li>";

   8:                 html += "<li>Email Address: " + contact.EmailAddress + "</li>";

   9:                 html += "</ul>";

  10:                 $("#contacts").append($(html));

  11:             });

  12:         }
   1:     </script>
   2: </head>

   3: <body>

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

   5:     <script type="text/javascript" src="http://localhost:3721/api/contacts?callback=listContacts"></script>

   5: </body>
   6: </html>

如果請求地址“http://localhost:3721/api/contacts?callback=listContacts”能夠返回如下的內容,即返回的不是以JSON表示的數據,而是針對該數據的方法調用,毫無疑問聯繫人列表能夠順利呈現在頁面上。這種將JSON對象填充(Padding)到某個JavaScript回調方法將數據轉換成針對數據的操作語句的形式就是JSONP(JSON Padding)。

   1: listContacts([{"Name":"張三","PhoneNo":"123","EmailAddress":"[email protected]"},{"Name":"李四","PhoneNo":"456","EmailAddress":"[email protected]"},{"Name":"王五","PhoneNo":"789","EmailAddress":"[email protected]"}]);

爲了讓定義在ContactsController的Action方法GetAllContacts返回如上所示的內容,我們可以對其相應的改動來完成。如下面的代碼片斷所示,我們將GetAllContacts的返回類型替換成HttpResponseMessage,其參數callback表示填充的JavaScript回調函數。在該方法中,我們利用JavaScriptSerializer對Contact列表對象進行序列化,並將得到的內容填充到回調函數中從而得到如上所示的內容。方法最終返回具有此主體內容的HttpResponseMessage對象,響應主體內容的媒體類型被設置爲“text/javascript”。

   1: public class ContactsController : ApiController
   2: {
   3:     public HttpResponseMessage GetAllContacts(string callback)
   4:     {
   5:         Contact[] contacts = new Contact[]
   6:         {
   7:             new Contact{ Name="張三", PhoneNo="123", EmailAddress="[email protected]"},
   8:             new Contact{ Name="李四", PhoneNo="456", EmailAddress="[email protected]"},
   9:             new Contact{ Name="王五", PhoneNo="789", EmailAddress="[email protected]"},
  10:         };
  11:         JavaScriptSerializer serializer = new JavaScriptSerializer();
  12:         string content = string.Format("{0}({1})", callback, serializer.Serialize(contacts));
  13:         return new HttpResponseMessage(HttpStatusCode.OK)
  14:         {
  15:             Content = new StringContent(content, Encoding.UTF8, "text/javascript")
  16:         };
  17:     }
  18: }

如果現在運行我們的程序,通過“跨域”(其實不是)調用Web API得到的聯繫人列表就會按照如右圖所示的效果呈現出來。JSONP僅僅是利用<script>的src標籤加載的腳本不受同源策略約束而採取的一種編程技巧,其本身並不是一種官方協議。並且並非所有類型跨域調用都能採用JSONP的方式來解決(由於所有具有src屬性的HTML標籤均通過HTTP-GET的方式來加載目標資源,這決定了JSONP只適用於HTTP-GET請求),所以我們必須尋求一種更好的解決方案。

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
發佈了38 篇原創文章 · 獲贊 12 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章