跨源資源共享 CORS
跨源資源共享(Cross-Origin Resource Sharing)是一種基于 HTTP 頭的機制持隧。出于安全性祖驱,瀏覽器限制腳本內發(fā)起的跨域請求, 例如板祝,XMLHttpRequest 和 Fetch API 遵循 同源策略(Same-origin policy)。CORS 機制允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源铭腕。
簡單請求
簡單請求(不會觸發(fā) CORS 預檢請求)需滿足所有下述條件:
- Request-Method 只能是 GET | HEAD | POST
- Request-Headers 允許人為設置的字段只能包含 Accept、Accept-Language多糠、Content-Language累舷、Content-Type
- Content-Type 只能是 text/plain | multipart/form-data | application/x-www-form-urlencoded
- XMLHttpRequest 對象沒有注冊任何事件監(jiān)聽器;XMLHttpRequest 對象可以使用 XMLHttpRequest.upload 屬性訪問
- 請求中沒有使用 ReadableStream 對象
簡單請求涉及的請求頭有 Origin熬丧、Access-Control-Allow-Origin笋粟、Access-Control-Expose-Headers、 Access-Control-Allow-Credentials等析蝴;
預檢請求(preflight request)
CORS要求害捕,那些可能對服務器數據產生副作用的 HTTP 請求,瀏覽器必須首先使用 OPTIONS
方法發(fā)起一個 預檢請求(preflight request)闷畸,從而獲知服務端是否允許該跨源請求尝盼。服務器確認允許之后,才發(fā)起實際的 HTTP 請求佑菩。
預檢請求頭字段
字段名稱 | 說明 |
---|---|
Origin | 表明預檢請求或實際請求的源站 domain |
Access-Control-Request-Headers | 用于預檢請求盾沫,將實際請求頭告訴服務器 |
Access-Control-Request-Method | 用于預檢請求,將實際請求方法告訴服務器 |
預檢響應頭字段
字段名稱 | 說明 |
---|---|
Access-Control-Allow-Origin | 指定允許訪問該資源的外域 URI殿漠,可以設置為***** 允許所有域的請求 |
Access-Control-Allow-Headers | 響應預檢請求赴精,指明了實際請求中允許攜帶的首部字段 |
Access-Control-Allow-Methods | 響應預檢請求,指明了實際請求所允許使用方法 |
Access-Control-Max-Age | 指定預檢請求的結果能夠被緩存多久绞幌,如果在有效期內蕾哟,再次請求將不會發(fā)起預檢請求 |
Access-Control-Allow-Credentials | 指明了實際的請求是否可以使用 credentials |
Access-Control-Expose-Headers | 服務器暴露一些自定義的相應頭,允許客戶端問(否則response是拿不到這些頭字段的) |
附帶身份憑證的請求(withCredentials)
一般情況,對于跨源 XMLHttpRequest 或 Fetch 請求莲蜘,瀏覽器不會發(fā)送身份憑證息(cookie)谭确。如果要把 Cookie 發(fā)到服務器,一方面要服務器同意票渠,指定 Access-Control-Allow-Credentials字段
Access-Control-Allow-Credentials: true
另一方面逐哈,開發(fā)者必須在 XMLHttpRequest 或 Fetch 請求中明確指明附帶身份憑證
// XMLHttpRequest withCredentials
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.other.com/resources', true);
xhr.withCredentials = true;
xhr.onreadystatechange = handler;
xhr.send();
// Fetch withCredentials
fetch('https://api.other.com/resources', {
mode: "cors",
credentials: "include"
});
服務器在響應附帶身份憑證的請求時:CORS 響應頭(Access-Control-Allow-Origin、Access-Control-Allow-Headers问顷、Access-Control-Allow-Methods)的值**不能設為通配符 *** 昂秃,而應將其設置為確定的值,否則會請求失敗杜窄。
Cookie 策略受 SameSite 屬性控制肠骆,如果 SameSite 值不是 None
,就算設置了withCredentials羞芍,cookie 也不會被發(fā)送到跨源的服務器哗戈。
響應頭中也可以攜帶 Set-Cookie 字段郊艘,嘗試對 Cookie 進行修改荷科。如果用戶瀏覽器的第三方 cookie 策略設置為拒絕所有第三方 cookies唯咬,那么會操作失敗,將會拋出異常畏浆。
設置允許跨站發(fā)送的cookie胆胰,但這樣可能導致 跨站請求偽造(Cross-site request forgery,CSRF)攻擊變得容易刻获。
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly; Secure; SameSite=None
跨域常見解決方案
1蜀涨、服務端直接配置 Access-Control-Allow-Origin
基于上述CORS機制,服務端配置成允許跨源請求就行
如果服務器未使用 *****蝎毡,而是指定了一個域厚柳,那么為了向客戶端表明服務器的返回會根據Origin請求頭而有所不同,必須在Vary響應頭中包含Origin
Access-Control-Allow-Origin: https://www.frontend.com
Vary: Origin
2沐兵、JSONP跨域
瀏覽器僅會限制腳本內發(fā)起的跨域請求别垮,而 script、img 標簽沒有跨域限制扎谎。所以可以通過script 標簽src屬性碳想,發(fā)送帶有callback參數的GET請求,服務端將接口返回數據放到callback函數中毁靶,返回給瀏覽器胧奔,瀏覽器的callback解析執(zhí)行,從而前端拿到callback函數返回的數據预吆。
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
function handleCallback(data) {
console.log('get data', data);
}
script.src = 'https://api.other.com/xx?callback=handleCallback';
document.head.appendChild(script);
</script>
3龙填、nginx 服務器代理
其實對于任一服務器都可以做代理轉發(fā)處理,nginx 大概是用得最多且最簡單的服務器代理了啡浊。
server {
listen 80;
server_name www.example.com;
location /api {
proxy_pass http://api.server.com; # 后端服務地址
}
location / {
root html/static; # 前端靜態(tài)資源路徑
# proxy_pass http://api.server.com; # 或前端服務地址
}
}
4觅够、構建工具代理(開發(fā)環(huán)境)
在日常開發(fā)中一般會直接使用 構建工具的代理(node server代理)配置,比如 webpack
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
},
},
},
};
5巷嚣、系統host添加域名
我們可以在系統的host文件 增加 ip = host
映射喘先,本地訪問域名host時,會去訪問真是ip地址廷粒,比如 127.0.0.1窘拯,一般在調試需要SSO登錄的應用的時候經常會用這種方式。
hosts文件位置在
Windows:C:\Windows\System32\drivers\etc\hosts
Mac:/etc/hosts
6坝茎、window.postMessage + iframe
window.postMessage 方法可以通過第二個參數 targetOrigin 安全地實現跨源通信涤姊。
示例:2個跨源頁面的通信
aaa.com/a.html 需要跨域請求數據的頁面
<iframe
src="http://bbb.com/b.html"
id="iframe"
></iframe>
<script>
const targetOrigin = 'http://bbb.com';
function receiveMessage(event) {
// 我們始終使用 origin 和 source 屬性驗證發(fā)件人的身份
if (event.origin !== targetOrigin) return;
console.log(event.data);
}
window.addEventListener("message", receiveMessage, false);
window.postMessage('fetchUserInfo', targetOrigin);
</script>
bbb.com/b.html 是同源的頁面
<script>
const targetOrigin = 'http://aaa.com';
function receiveMessage(event) {
if (event.origin !== targetOrigin) return;
if (event.data === 'fetchUserInfo') {
const user = { /** mock data */ };
window.postMessage(JSON.stringify(user), targetOrigin);
}
}
window.addEventListener("message", receiveMessage, false);
</script>
以上是最常見的幾種跨域解決方案,還有一些不太常用的方法 比如
- document.domain + Iframe(只能用于二級域名相同的情況下)
- window.location.hash + Iframe
- window.name+ Iframe
- Websocket