[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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="http://api.mydomain/jsonp.php?callback=jsonpCallback&id=xxx"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function jsonpCallback(data){ | |
alert(data.msg); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
header("Content-Type: applicaton/json"); | |
//proccess data query | |
$data = array("msg"=>$_GET["id"]." is back"); | |
echo $_GET["callback"]."(".json_encode($data).")" | |
?> |
幸虧了 jQuery ,使用 jsonp 將不再這麼麻煩,要發送跨域請求時只需要在 $.ajax() 代入參數 dataType="jsonp" ,並在 url 加入參數 callback=xxxx 即可,當然後端也必須作相對應的配合 (將資料以傳入的 callback 參數以 function call 型式包裝起來),回傳後 jQuery 會自動將 response data 傳給 success callback,省去自定義 callback 步驟,如下所示:
JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$.ajax({ | |
url: "http://api.mydomain/jsonp.php?callback=test", | |
dataType:"jsonp", | |
success: function(response){ | |
console.log(response); | |
}, | |
error: function(response){ | |
console.log("error"); | |
} | |
}); |
PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
header("Content-Type: application/json"); | |
echo $_GET["callback"]."(".json_encode(array("email"=>"nono@gmail.com","auth_token"=>"cdefgh")).")"; | |
?> |
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 請求)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//assume origin domain = http://www.mydomain | |
$.ajax({ | |
url: "http://api.mydomain/api.php", | |
success: function(response){ | |
console.log(response); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
header("Access-Control-Allow-Origin: http://www.mydomain"); | |
header("Content-Type: application/json"); | |
echo json_encode(array("msg"=>"CORS is awesome!")); | |
?> |
上述三種方法都可以用來解決 xhr cross domain access 的問題,但不同解法有不同限制,像是 jsonp 與 cors 需要 response 端的配合,如果是第三方服務且沒有支援這兩種方法的情況可能使用 proxy 才可能解決問題,或是考慮服務所支援的瀏覽器是否皆有實作 cors…等等,都是採用哪種方法所要考量的。
留言
張貼留言