[JavaScript] 前端跨網域存取問題 (xhr cross-domain request is not allowed)

XMLHttpRequest cannot load ... Origin ... is not allowed by Access-Control-Allow-Origin

這個問題常發生在前端程式嘗試以 AJAX (XMLHttpRequest) 方式存取跨網域資源時,因為 Security 的考量,造成 request 發送失敗的情況。


Same Origin Policy

Same Origin Policy 是現代瀏覽器在安全性上的一個重要設計,主要是確保 script 只能在與其載入來源相同的頁面執行,達到不同 origin 網站彼此無法互相干擾。(註: 這裡指的相同 origin 是指 domain name, protocol, port 皆相同)。

對於 XMLHttpRequest 也有類似規範,當 client 端的 JS 對遠端主機發送 XMLHttpRequest 時,因為會挾帶瀏覽器所 maintain 的 cookie 資訊,因此只能對相同 origin 的 resource 進行存取(也就是 AJAX 呼叫的 url 必須與目前所在頁面相同),此限制確保了其存取的安全性。

為什麼需要 Cross-domain 存取?

因為前端技術的快速發展成熟,使得前端與後端之間的分野愈來愈鮮明,前端負責頁面呈現與動態互動,後端負責演算與 API 服務介接,因此在開發這類型網站時,常會整合第三方的服務( e.g. flickr api 取得照片)或重覆利用自己先前開發的後端 API,但這就違反了 Browser 的 Same Origin Policy,因為不論是第三方或先前所開發的服務,通常都與網站位址不相同,所以處理跨網域資源存取的問題是必備的技能。

解決方案

1. AJAX Proxy

屬於 Server 端的解決方案,簡單來說就是將 web server 當成前端瀏覽器與其它第三方伺服器之間溝通的中介,browser 發送 AJAX request 給 server 端 proxy,proxy 再將 request 轉送給第三方服務並取得內容回傳給前端。通常有下列兩種實作策略:

(1) 前端完全不需知道使用什麼第三方服務,只需要存取後端 proxy 提供的方法,ajax proxy 會負責轉送 request 至第三方服務,取得結果並回傳給前端,讓前端有此服務就是後端 server 提供的感覺。

(2) 前端必須知道第三方服務的 url,並在發送 request 時以參數的方式傳遞到後端,後端僅提供一個介面將指定的 url 轉送並負責將結果回傳,這種方法讓開發者有瀏覽器沒有 policy 限制的感覺。

2. JSONP

JSONP = JSON with padding or prefix,第一次聽到 JSONP 時,可能會對它產生誤解,以為是另一種資料格式,但其實 JSONP 是一種避開 Same Origin Policy 發送 cross-domain request 的技巧。

其運作原理是利用 script tag 的 open policy,意思就是 browser 對於載入任何網域的 js native code 沒有限制,藉由這個特性將所需的 json data 以 function call 包起來,而這個 function 是被 execution context 所定義好的,當 js code 被以 script element 的方式 inject 到 html 時,就可以順利將後端資料帶入處理函式,達到跨網域請求資料的目的。

例如:

假如現在 mydomain 需要跟 api.mydomain 取得資料 (jsonp.php),那 jsonp 的實作方式如下

HTML
JS
PHP
透過上述例子可以很簡單的了解 jsonp 的原理,HTML 以 script injection 方式呼叫跨域 API 並帶入參數跟 callback 名稱,後端接到參數後,取出 json 格式的資料,並以 callback 包裝起來回傳給前端,前端因為在 script block 已經定義了該 callback 函式,所以當 native js code 被載入時,會很神奇的把資料塞給處理函式,完成了跨域呼叫。

幸虧了 jQuery ,使用 jsonp 將不再這麼麻煩,要發送跨域請求時只需要在 $.ajax() 代入參數 dataType="jsonp" ,並在 url 加入參數 callback=xxxx 即可,當然後端也必須作相對應的配合 (將資料以傳入的 callback 參數以 function call 型式包裝起來),回傳後 jQuery 會自動將 response data 傳給 success callback,省去自定義 callback 步驟,如下所示:

JS

PHP

3. CORS

CORS (Cross-Origin Resource Sharing) 是 W3C 針對不同源的 Browser 跟 Server 之間進行構通所制訂的 protocol,其實作原理就是透過兩個 Custom http header (Origin and Access-Control-Allow-Origin) 來確認雙方是否可以成功發送 Request 並回傳,client 端在發送 request 時 (僅限支援 CORS 的 Browser),如果發現請求目標非同一 domain,會自動加入 custom header: Origin 並傳入自己的 domain,而當 Server 接收請求並回傳時,必須加上 "Access-Control-Allow-Origin: http://domain/allowed" 這個 header,帶入的值就是允許跨域請求的白名單,如果是星號 (*) 代表允許全部,如果有安全性考量要避免使用。

上述解法僅限 Simple Request,何謂 Simple request 在 W3C 官網有明確的規範,只要 Client 發送的請求符合下列條件都算是簡單的請求:


  • HTTP method:
    • Head
    • Get
    • Post
  • HTTP headers:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: application/x-www-form-urlencode
    • Content-Type: multipart/form-data
    • Content-Type: text/plain


如果請求的內容有超出上述範圍(例如: PUT method, Content-Type: application/json) 就必須用另外的處理方法,但礙於篇幅,有機會再獨立寫一篇文章介紹。最後再以一個簡單的例子示範 CORS 用法:

JS (只需要簡單發送一個跨域的 AJAX 請求)
PHP (回傳允許跨域存取的HEADER)
總結

上述三種方法都可以用來解決 xhr cross domain access 的問題,但不同解法有不同限制,像是 jsonp 與 cors 需要 response 端的配合,如果是第三方服務且沒有支援這兩種方法的情況可能使用 proxy 才可能解決問題,或是考慮服務所支援的瀏覽器是否皆有實作 cors…等等,都是採用哪種方法所要考量的。

留言

這個網誌中的熱門文章

[Android] 內部儲存體(Internal Storage)的檔案系統讀寫(File I/O)

[Android] layout_weight的妙用-讓View的大小以百分比率顯示(proportionate size)

【海外婚紗】道具行李篇