構建public APIs與CORS

在構建Public APIs的過程當中,首先要解決的第一個問題就是跨域請求的問題。html

網絡應用安全模型中很重要的一個概念是「同源準則」(same-origin policy)。該準則要求一個網站(由協議+主機名+端口號三者肯定)的腳本(Script)、XMLHttpRequest和Websocket無權去訪問另外一個網站的內容。在未正確設置的狀況下,跨域訪問會提示以下錯誤:No 'Access-Control-Allow-Origin' header is present on the requested resource. 這項限制對於跨域的Ajax請求帶來了不少不便。html5

典型的對於跨域請求的解決方案以下:jquery

  • document.domain property
  • Cross-Origin Resource Sharing (CORS)
  • Cross-document messaging
  • JSONP

本文重點講述的則是其中Cross-Origin Resource Sharing (CORS)的原理和在rails下的配置方式

ajax

Cross-Origin Resource Sharing (CORS)

CORS的基本原理是經過設置HTTP請求和返回中header,告知瀏覽器該請求是合法的。這涉及到服務器端和瀏覽器端雙方的設置:請求的發起(Http Request Header)和服務器對請求正確的響應(Http response header)。chrome

發起CORS請求

CORS兼容如下瀏覽器:json

  • Internet Explorer 8+
  • Firefox 3.5+
  • Safari 4+
  • Chrome

原生Javascript能夠經過XMLHttpRequest Object或XDomainRequest發起請求,詳細的方式能夠參見這篇文章:http://www.html5rocks.com/en/tutorials/cors/api

JQuery的$.ajax()能夠用來發起XHR或者CORS請求。然而該方法不支持IE下的XDomainRequest,須要使用JQuery的插件來實現IE下的兼容性(http://bugs.jquery.com/ticket/8283跨域

$.ajax({

      // The 'type' property sets the HTTP method.
      // A value of 'PUT' or 'DELETE' will trigger a preflight request.
      type: 'GET',

      // The URL to make the request to.
      url: 'http://updates.html5rocks.com',

      // The 'contentType' property sets the 'Content-Type' header.
      // The JQuery default for this property is
      // 'application/x-www-form-urlencoded; charset=UTF-8', which does not trigger
      // a preflight. If you set this value to anything other than
      // application/x-www-form-urlencoded, multipart/form-data, or text/plain,
      // you will trigger a preflight request.
      contentType: 'text/plain',

      xhrFields: {
        // The 'xhrFields' property sets additional fields on the XMLHttpRequest.
        // This can be used to set the 'withCredentials' property.
        // Set the value to 'true' if you'd like to pass cookies to the server.
        // If this is enabled, your server must respond with the header
        // 'Access-Control-Allow-Credentials: true'.
        withCredentials: false
      },

      headers: {
        // Set any custom headers here.
        // If you set any non-simple headers, your server must include these
        // headers in the 'Access-Control-Allow-Headers' response header.
      },

      success: function() {
        // Here's where you handle a successful response.
      },

      error: function() {
        // Here's where you handle an error response.
        // Note that if the error was due to a CORS issue,
        // this function will still fire, but there won't be any additional
        // information about the error.
      }
    });

服務器正確響應CORS請求

根據請求內容的不一樣,瀏覽器會須要添加對應的Header或者發起額外的請求。其中的細節都由瀏覽器負責處理,對於用戶來說是透明的。咱們只須要了解如何針對差別的請求作出適當的響應便可。
咱們將CORS請求分紅如下兩種類型:
一、簡單請求
二、不是那麼簡單的請求瀏覽器

其中簡單請求要求:
請求類型必須是GET,POST,HEAD三者中的一種
請求頭(Header)中僅能夠包含:緩存

  • Accept
  • Accept Language
  • Content Language
  • Last Event ID
  • Content Type:僅接受application/x-www-form-urlencoded,multipart/form-data,text/plain

不知足上述條件的全部請求,例如PUT,DELETE或者是Content Type是application/json,均爲「不是那麼簡單的請求」。針對這種請求,瀏覽器會在真實請求前,額外發起一次類型爲OPTIONS的請求(Preflight request),只有服務器正確響應了OPTIONS請求後,瀏覽器纔會發起該請求。(參見下圖)

002b3Mi3gy6IR7E5VmI1c

下文將針對b.com向a.com發起跨域請求說明服務器如何正確響應這兩種類型的請求。

簡單請求

瀏覽器在發出請求前爲請求添加Origin來標明請求的來源,用戶不可更改此內容。但Header中是否有Origin並不能做爲判斷是不是CORS請求的標準,由於不一樣瀏覽器對於此內容的處理方式並不徹底一致,同源請求中也有可能出現Origin。
下面是一個b.com向a.com發起的一次GET請求。

GET /cors HTTP/1.1
Origin: http://b.com
Host: a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

正確響應的返回以下,均由Access-Control-*開頭:

Access-Control-Allow-Origin: http://b.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

Access-Control-Allow-origin: 此處是Server贊成跨域訪問的域名列表。若是容許任意網站請求資源,此處能夠寫爲'*'
Access-Control-Expose-Headers: 能夠設置返回的Header以傳遞數據。簡單請求中容許使用的Header包括:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma。

不是那麼簡單的請求

若是但願使用PUT,DELETE等RESTful等超出了簡單請求的範圍的請求,瀏覽器則會在發起真實請求前先向服務器發起一次稱做Preflight的OPTIONS的請求,以確保服務器接受該類型請求。其後纔會發起真實要求的請求。請求的發起與簡單請求並沒有差別,而服務器端則要針對Preflight Request作額外的響應。
下面是一次典型的Preflight請求:

OPTIONS /cors HTTP/1.1
Origin: http://b.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Access-Control-Request-Method表明真實請求的類型。Access-Control-Request-Headers則表明真實請求的請求頭key內容。服務器僅在驗證了這兩項內容的合法性以後纔會贊成瀏覽器發起真實的請求。

Access-Control-Allow-Origin: http://b.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8

此處並未列舉的一項返回頭是Access-Control-Max-Age。由於每次請求均要發起一次額外的OPTIONS請求是很是低效的,所以能夠爲瀏覽器保存該返回頭設置一個緩存的時間,單位爲秒。在緩存過時之前,瀏覽器無需再次驗證同一類型的請求是否合法。

真實請求的內容則和簡單請求的內容徹底一致,此處再也不贅述。



下圖很是詳細的再次描述了服務器對於不一樣類型的請求如何作出正確的響應。
002b3Mi3gy6IRKkSXEPdf


Rails下對CORS請求的配置

首先要確保在Routes.rb中加上對於OPTIONS請求的正確響應。
OPTIONS請求會發至真實請求的同一位置。若是未正確設置route,則會出現404沒法找到請求地址的錯誤。
響應該請求的Controller的action方法能夠設置爲空,由於該請求的關鍵僅是正確返回請求頭。
例如:真實請求/api/trips PUT,OPTIONS請求將發送至/api/trips OPTIONS。
match '/trips', to: 'trips#index', via: [:options]
或者可使用:
match '*all' => 'application#cor', :constraints => {:method => 'OPTIONS'}

確保了OPTIONS請求能夠正確被響應以後,在applicationController.rb中以下配置:

before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers

def cors_set_access_control_headers
      headers['Access-Control-Allow-Origin'] = '*'
      headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, OPTIONS'
      headers['Access-Control-Max-Age'] = '1728000'
end

def cors_preflight_check
    if request.method == 'OPTIONS'
      headers['Access-Control-Allow-Origin'] = '*'
      headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
      headers['Access-Control-Request-Method'] = '*'
      headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
      headers['Access-Control-Max-Age'] = '1728000'
      render :text => '', :content_type => 'text/plain'
    end
end

對於簡單請求,由cors_set_access_control_headers作出正確的響應。對於不是那麼簡單的請求,cors_preflight_check則會發現若請求是OPTIONS的時候,在實際執行cors_set_access_control_headers以前,攔截下該請求並返回text/plain的內容和正確的請求頭。


總結

CORS請求做爲構建Public API中很重要的一環,理解其大體的工做原理仍是很是有意義的。不過在Chrome中,時常會出現provision header shown這樣奇怪的錯誤,而這個錯誤出現的緣由說法不一,基本上能夠理解爲跨域訪問過程當中若是請求出現問題chrome並無辦法很好的瞭解錯誤緣由,沒法準確的給出錯誤狀態。另外,CORS調試也是一個問題,基於非瀏覽器的POSTMAN調試,有時不可以準確的反映出請求在瀏覽器下的真實工做狀態,不知道如何才能更有效果的測試和調試CORS。
有興趣的話,還能夠進一步經過https://dvcs.w3.org/hg/cors/raw-file/tip/Overview.html瞭解W3C的CORS協議內容。http://arunranga.com/examples/access-control/


Reference:
http://en.wikipedia.org/wiki/Same_origin_policy
http://www.html5rocks.com/en/tutorials/cors/
http://www.nczonline.net/blog/2010/05/25/cross-domain-ajax-with-cross-origin-resource-sharing/
http://www.tsheffler.com/blog/?p=428
http://blog.rudylee.com/2013/10/29/rails-4-cors/
http://stackoverflow.com/questions/17858178/allow-anything-through-cors-policy

相關文章
相關標籤/搜索