一文帶你瞭解跨域原理與解決辦法
跨域,指的是從一個域名去請求另外一個域名的資源,即跨域名請求。跨域時,瀏覽器不能執行其他域名網站的腳本,這是由瀏覽器的同源策略造成的,是瀏覽器施加的安全限制, 跨域限制訪問,其實是瀏覽器的限制。 同源策略是瀏覽器最核心也最基本的安全功能,不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方資源 ,這是一個用於隔離潛在惡意文件的重要安全機制。所以跨域問題只在瀏覽器中出現,如果客戶端是APP的話,那跨域問題就不存在了。 PS:IE端口除外,IE對同源策略的定義有略微的不同,具體可以查看文末給出的同源策略的鏈接。
所謂同源是指:域名,協議,端口相同,即兩個資源具有相同的源。 只要三者之間有一個不同,就是跨域(跨源)。
URL | 說明 | 同源檢測結果 | 是否跨域 |
---|---|---|---|
http://www.abc.com/a.html http://www.abc.com/b.html |
域名 端口 協議相同 | 成功 | 否 |
http://www.abc.com/dir1/a.html http://www.abc.com/dir2/b.html |
域名 端口 協議相同,路徑不同 | 成功 | 否 |
http://www.abc.com:8080/a.html http://www.abc.com/b.html |
域名 協議相同,端口不同 | 失敗 | 是 |
http://www.abc.com/a.html https://www.abc.com:80/b.html |
域名 端口相同,協議不同 | 失敗 | 是 |
http://www.abc.com/a.html http://59.68.92.100/b.html |
域名和域名對應的IP | 失敗 | 是 |
http://www.abc.com/a.html http://blog.abc.com/b.html |
二級域名相同,三級域名不同 | 失敗 | 是 |
http://www.abc.com/a.html http://abc.com/b.html |
二級域名相同,三級域名不同 | 失敗 | 是 |
http://www.abc.com/a.html http://www.aaa.com/b.html |
協議 端口相同,域名不同 | 失敗 | 是 |
1 非同源限制
- 無法讀取非同源網頁的
Cookie
、LocalStorage
和IndexedDB
- 無法對非同源網頁的
DOM
和JS
對象進行操作 - 無法向非同源地址發送
AJAX
請求
注:本文只專注於AJAX請求跨域,其他跨域類型不做過多深入。
2 實現跨域的解決方案
實現跨域的方式有很多種,比如JSONP、CORS、http-proxy、nginx、websocket
、跨站腳本API訪問,如:postMessage、document.domain
等。
2.1 JSONP
由於同源策略的限制,AJAX
請求是不允許進行跨域請求的,但是在HTML中
,擁有src
和href
屬性的標籤是可以跨域請求外部資源的,如link、script、img
等(值得注意的是,不同標籤允許的交互類型貌似是不同的,分別爲跨域寫、跨域資源嵌入、跨域讀,暫時不知道這些標籤可以發送跨域請求的原因,貌似是歷史遺留問題,有知道的大佬可以指點一下),根據<script>
標籤的特性,開發人員想到了一個解決跨域請求的方法,即JSONP
,全名 JSON with padding
。
爲了方便進行實驗(其實是爲了偷懶),我找了一個百度的JSONP
接口https://www.baidu.com/sugrec?prod=pc&wd=什麼是JSONP&cb=getData
,該接口的特點是:你輸入一個指定的函數名,然後服務器會根據函數名返回一串JS
的函數調用格式的字符串,比如我們指定的函數名爲getData
,然後百度的服務器會根據我們指定的函數名返回如下內容:
getData({"q":"什麼是jsonp","p":true,"g":[{"type":"sug","sa":"s_1","q":"什麼是jsonp異步處理方法"}],"slid":"16695766345172117423"})
讓我們仔細觀察一下服務器返回的內容,你是否發現這種格式很熟悉?這不就是 函數名 + (+ 參數 +)
的格式嗎?如果我們有一個名爲getData
、形參是一個對象的JS
函數,是不是就意味着我們可以把服務器返回的數據看成是一段調用了一個函數名爲getData
、形參是一個對象的函數的JS
代碼呢?答案是顯而易見的,肯定是可以的嘛。那怎麼讓服務器返回的數據變成一段JS
代碼呢?我們都知道,我們可以在HTML頁面裏面編寫JS
代碼——只需將JS
代碼用<script>
標籤括起來就可以讓代碼在頁面加載的時候就運行了。前面我們也提到了<script>
標籤的src
屬性是可以跨域請求外部資源的,如果我們將我們要訪問的接口做爲src
屬性的值,那我們不就可以訪問該跨域接口了嗎?如果我們再提供一個全局的函數getData
,用來對接口返回的數據進行操作,那豈不是就實現了跨域請求?是的,確實可以,而這就是JSONP
的實現原理。這裏的getData
函數就是一個callback函數。讓我們整理一下JSONP
的原理:
- 使用script 標籤發送請求,這個標籤支持跨域訪問
- 在script 標籤裏面給服務器端傳遞一個 callback
- callback 的值對應到頁面一定要定義一個全局函數(爲什麼是全局?因爲服務端接收到callback函數後會返回頁面中的script中去找,如果不寫在全局作用域中根本找不到)
在瞭解完JSONP
的原理之後,我們來看看具體的JS
代碼實現:
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
//
let scriptDom = document.createElement('script');
//定義一個全局函數,函數名爲cb的值,此函數會由服務器的內容來調用
window[cb] = function (data) {
resolve(data);
document.body.removeChild(scriptDom);
};
params = {...params, cb}
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`);
}
scriptDom.src = `${url}?${arrs.join('&')}`;
//爲了讓代碼執行需要script對象添加Dom樹中
document.body.appendChild(scriptDom);
})
};
jsonp({
url: 'https://www.baidu.com/sugrec',
params: {prod: 'pc', wd: '什麼是JSONP'},
cb: 'getData'
}).then(data => {
console.log(data);
});
代碼所做的事情就是我們在前面列出來的三點,當然爲了方便我是用了Promise來寫,不瞭解這個的可以去網上搜一下,或者是查一下別的JSONP
代碼的實現,當然代碼不是最重要的,理解了原理,代碼寫起來也就不難了。
爲了方便大家理解服務器所幹的事情,我也寫了一段PHP代碼供大家參考,如果大家需要其他語言版本的,去百度或者Google找一下就行了。
<?php
$_call = $_GET["cb"];
$_q = $_GET["wd"];
$data = {"q":$_q,"p":true,"g":[{"type":"sug","sa":"s_1","q":"什麼是jsonp異步處理方法"}],"slid":"16695766345172117423"}
echo $_call."(".$data.")";
?>
簡單地說就是將前端傳過來的方法名和關鍵詞拼接成一串JS
函數調用格式的字符串返回。
缺點:只支持GET請求,不支持其他請求,有XSS攻擊的風險。
爲了方便總結,我還簡單的畫了一個JSONP
的流程圖:
2.2 CORS
同源策略默認阻止“跨域”獲取資源。但是跨域資源共享CORS
給了web服務器這樣的權限:服務器可以選擇是否允許跨域請求訪問到它們的資源。
跨域資源共享(CORS
)是一種機制, 它由一系列的HTTP
頭組成,這些HTTP
頭決定瀏覽器是否阻止前端 JavaScript
代碼獲取跨域請求的響應,從而克服了AJAX
只能同源使用的限制。
跨域時,瀏覽器會讓請求帶上Origin請求頭,表明請求來自哪個站點;而服務器必須要讓響應帶上允許跨域訪問的Access-Control-Allow-Origin響應頭,表明允許某個站點可以進行訪問該服務器。
2.2.1 簡單請求
瀏覽器將CORS請求分成兩類:簡單請求和非簡單請求。怎麼區分這兩者呢?我們先來看兩個條件:
(1)HTTP請求方法是以下三種之一:
·HEAD
·GET
·POST
(2)只包含簡單HTTP請求頭,即:
·Accept,
·Accept-Language,
·Content-Language,
·Content-Type並且值是 application/x-www-form-urlencoded, multipart/form-data, 或者 text/plain之一的(忽略參數)。
當請求滿足上面的兩個條件時,則該請求被視爲簡單請求,否則被視爲非簡單請求。簡單請求與非簡單請求的最主要區別就是跨域請求是否需要發送預檢請求(preflight request)。
在進行跨域請求時,如果是簡單請求,則瀏覽器會在請求中增加一個Origin請求頭之後直接發送CORS
請求,服務器檢查該請求頭的值是否在服務器設置的CORS
許可範圍內,如果在許可範圍內,則服務器同意本次請求,如果不在許可範圍內,則服務會返回一個沒有包含Access-Control-Allow-Origin
響應頭的HTTP
響應。值得注意的是,該響應的狀態碼還是200,所以無法通過狀態碼來識別是否跨域成功,但是瀏覽器在發現跨域請求的響應頭沒有包含 Access-Control-Allow-Origin
響應頭時,就知道跨域失敗了,這時瀏覽器會拋出一個錯誤,這個錯誤會被XMLHttpRequest
請求的onerror
回調函數捕獲。下面兩張圖分別是在進行簡單請求時,服務器允許跨域和不允許跨域時響應頭的情況:
$("#visit").click(function () {
$.ajax({
url: 'http://localhost:8080/crossDomain/testCross',
contentType: "application/x-www-form-urlencoded",
type: 'get',
crossDomain:true,
success: function (res) {
console.log("哇哦,跨域成功了!")
console.log(res)
alert(JSON.stringify(res))
},
//跨域失敗時,瀏覽器拋出的錯誤會被此回調函數捕獲
error: function (e) {
console.log("哇哦,跨域失敗了!")
console.log(e)
}
});
});
從代碼可以看出,我們在跨域失敗時瀏覽器的狀態捕獲了下來並將該錯誤在瀏覽器控制檯打印了出來,下圖是我們在瀏覽器控制檯看到的輸出信息,紅色的是瀏覽器的報錯信息,大概意思就是我們請求的資源沒有Access-Control-Allow-Origin
響應頭,所以這次XMLHttpRequest
請求被CORS
策略阻斷了。此外我們也可以看到AJAX
的readyState
的值是0,表明瀏覽器認爲這次跨域請求是沒有發出去的(實際上已經請求成功了,但是瀏覽器阻止了JavaScript
讀取訪問到的內容)
2.2.2 非簡單請求
如果是非簡單請求,則瀏覽器會先發起一次預檢請求(OPTIONS請求),瀏覽器除了會帶上Origin請求頭之外,還會再帶上Access-Control-Request-Method 和 Access-Control-Request-Headers 這兩個請求頭,服務器在收到預檢請求之後,會檢查這三個請求頭是否與服務器的資源設置(接口)一致,如服務器的接口只允許請求方法爲GET
、Origin
爲http://www.abc.com:8080
、Access-Control-Request-Headers
爲 content-type
的請求,只要預檢請求中三個請求頭有任意一個值與服務器的資源(接口)設置不一致,服務器就會拒絕預檢請求,如果都一致,則服務器確認通過預檢請求並返回帶有Access-Control-Allow-Credentials、Access-Control-Allow-Headers、Access-Control-Allow-Methods、Access-Control-Allow-Origin、Access-Control-Max-Age、Allow
等響應頭的相應,這些響應頭的作用可以看一下文章後面的附錄,這裏不再贅述。下面兩張圖分別是在進行預檢請求時,服務器允許跨域和不允許跨域時響應頭的情況:
如果預檢請求通過,則瀏覽器會發送一個正常的和預檢請求同名的跨域請求,如帶自定義請求頭的POST、GET的非簡單請求或DELETE、PUT請求,否則返回的響應碼爲403,表示不允許請求,此時瀏覽器不會再發送同名的跨域請求。
我們還可以在有的瀏覽器中看到請求頭帶有Sec-Fetch-Mode和Sec-Fetch-Site這兩個請求頭,比如Chrome,這是用來標識此次請求的請求模式(跨域規則與瀏覽上下文)和是否跨域。
最後給出一張流程圖:
2.3 HTTP-Proxy
前面提到過,同源策略是瀏覽器施加的安全限制,它只存在於瀏覽器中。因此,我們可以在前端服務器與後端服務器之間加一個代理中間件(比如Node中間件)來實現,通過代理中間件轉發請求,從而達到跨域請求的目的。Node中間件代碼如下:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目標接口
target: 'http://www.end.com:8080',
changeOrigin: true,
// 修改響應頭信息,實現跨域並允許帶cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.front.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改響應信息中的cookie域名
cookieDomainRewrite: 'www.front.com' // 可以爲false,表示不修改
}));
app.listen(3000);
2.4 Nginx
2.4.1 Nginx配置解決iconfont跨域
前面我們提過,瀏覽器跨域訪問js、css、img
等常規靜態資源是被同源策略許可的,但iconfont
字體文件(eot|otf|ttf|woff|svg)
例外,這些文件是不會被允許的跨域訪問的,此時可在Nginx
的靜態資源服務器中加入以下配置。
location ~* \.(eot|ttf|woff|svg|otf)$ {
add_header Access-Control-Allow-Origin *;
}
或者是:
location / {
add_header Access-Control-Allow-Origin *;
}
2.4.2 Nginx反向代理接口跨域
我們還可以通過Nginx
配置一個代理服務器來轉發請求,反向代理訪問後臺接口,並修改cookie中域名信息,從而實現跨域攜帶cookie。
server {
listen 80;
server_name www.front.com;
location / {
proxy_pass http://www.end.com:8080; #反向代理
proxy_cookie_domain www.end.com www.front.com; #將cookie裏的域名修改爲前端的域名
index index.html index.htm;
# 如果不是瀏覽器直接訪問Nginx時,下面的跨域配置可不啓用,下面配置是爲了添加響應頭
add_header Access-Control-Allow-Origin http://www.front.com; #當前端只進行跨域不需要攜帶cookie時,可爲*,否則不能爲*,具體看後面附錄補充的請求頭的說明
add_header Access-Control-Allow-Credentials true;
}
}
2.5 WebSocket
瀏覽器允許腳本直連一個WebSocket地址而不遵循同源策略,所以我們可以通過使用WebSocket協議來實現跨域。具體代碼如下:
前端代碼:
var socket = io('http://www.front.com:8080');
// 連接成功處理
socket.on('connect', function() {
// 監聽服務端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 監聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
後臺代碼(NodeJS版,其他版本可以去百度或者Google找):
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
// 監聽socket連接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 斷開處理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
2.6 其他
其他的跨域解決方案這裏就不過多闡述了,這裏給出相關的參考資料,感興趣的可以看看。
postMessage
:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
Document.domain
:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain
localStorage和cookie的跨域解決方案
:https://www.cnblogs.com/vsmart/p/9388597.html
附錄
1. AJAX的readyState狀態值說明
在AJAX
實際運行當中,對於訪問XMLHttpRequest(XHR)
時並不是一次完成的,而是分別經歷了多種狀態後取得的結果,對於這種狀態在AJAX
中共有5種,分別是:
0 - (未初始化)還沒有調用send()
方法
1 - (載入)已調用send()
方法,正在發送請求
2 - (載入完成)send()
方法執行完成,
3 - (交互)正在解析響應內容
4 - (完成)響應內容解析完成,可以在客戶端調用了
對於上面的狀態,其中“0”狀態是在定義後自動具有的狀態值,而對於成功訪問的狀態(得到信息)我們大多數採用“4”進行判斷。
2. AJAX的status狀態碼說明
1xx:請求收到,繼續處理
2xx:操作成功收到,分析、接受
3xx:完成此請求必須進一步處理
4xx:請求包含一個錯誤語法或不能完成
5xx:服務器執行一個完全有效請求失敗
100——客戶必須繼續發出請求
101——客戶要求服務器根據請求轉換HTTP
協議版本
200——交易成功
201——提示知道新文件的URL
202——接受和處理、但處理未完成
203——返回信息不確定或不完整
204——請求收到,但返回信息爲空
205——服務器完成了請求,用戶代理必須復位當前已經瀏覽過的文件
206——服務器已經完成了部分用戶的GET
請求
300——請求的資源可在多處得到
301——刪除請求數據
302——在其他地址發現了請求數據
303——建議客戶訪問其他URL
或訪問方式
304——客戶端已經執行了GET
,但文件未變化
305——請求的資源必須從服務器指定的地址得到
306——前一版本HTTP中使用的代碼,現行版本中不再使用
307——申明請求的資源臨時性刪除
400——錯誤請求,如語法錯誤
401——請求授權失敗
402——保留有效ChargeTo
頭響應
403——請求不允許
404——沒有發現文件、查詢或URl
405——用戶在Request-Line
字段定義的方法不允許
406——根據用戶發送的Accept
拖,請求資源不可訪問
407——類似401,用戶必須首先在代理服務器上得到授權
408——客戶端沒有在用戶指定的餓時間內完成請求
409——對當前資源狀態,請求不能完成
410——服務器上不再有此資源且無進一步的參考地址
411——服務器拒絕用戶定義的Content-Length
屬性請求
412——一個或多個請求頭字段在當前請求中錯誤
413——請求的資源大於服務器允許的大小
414——請求的資源URL長於服務器允許的長度
415——請求資源不支持請求項目格式
416——請求中包含Range
請求頭字段,在當前請求資源範圍內沒有range
指示值,請求也不包含If-Range
請求頭字段。
417——服務器不滿足請求Expect
頭字段指定的期望值,如果是代理服務器,可能是下一級服務器不能滿足請求。
500——服務器產生內部錯誤
501——服務器不支持請求的函數
502——服務器暫時不可用,有時是爲了防止發生系統過載
503——服務器過載或暫停維修
504——關口過載,服務器使用另一個關口或服務來響應用戶,等待時間設定值較長
505——服務器不支持或拒絕支請求頭中指定的HTTP
版本
3. 幾個重要的Http頭
Origin: 存在於請求中**,**用於指明當前請求來自於哪個站點, Origin
僅僅包含站點信息,不包含任何路徑信息。
Host:客戶端指定自己想訪問的HTTP
服務器的域名/IP
地址和端口號。
Referer: 當瀏覽器向web服務器發送請求的時候,一般會帶上Referer
,告訴服務器該網頁是從哪個頁面鏈接過來的,服務器因此可以獲得一些信息用於處理。
Access-Control-Allow-Origin: 它的值要麼是請求時Origin
字段的值,要麼是一個*
,表示接受任意域名的請求 。
Access-Control-Expose-Headers :CORS
請求時,XMLHttpRequest
對象的getResponseHeader()
方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
。如果想拿到其他字段,就必須在Access-Control-Expose-Headers
裏面指定。
Access-Control-Request-Method : 用來列出瀏覽器的CORS
請求會用到哪些HTTP方法 。
Access-Control-Request-Headers : 指定瀏覽器CORS
請求會額外發送的頭信息字段。
Access-Control-Allow-Methods: 它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。
Access-Control-Allow-Headers: 表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。
Access-Control-Allow-Credentials: 它的值是一個布爾值,表示是否允許瀏覽器發送請求時帶上Cookie
。默認情況下,Cookie
不包括在CORS
請求之中。設爲true
,即表示服務器明確許可,Cookie
可以包含在請求中,一起發給服務器。這個值也只能設爲true
,如果服務器不要瀏覽器發送Cookie
,刪除該字段即可。 值得注意的是:Access-Control-Allow-Credentials爲true時,Access-Control-Allow-Origin 的值不能爲 *
Access-Control-Max-Age:用來指定本次預檢請求的有效期,單位爲秒。 非簡單請求每次會發出兩條請求,這樣自然會影響我們的效率,HTTP
協議裏面增加了一個響應頭可以用來緩存我們的預檢命令,這樣在緩存有效內就不再需要發送預檢請求了。
更多參考:
瀏覽器的同源策略:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
跨域資源共享:https://developer.mozilla.org/zh-CN/docs/Glossary/CORS
簡單頭部:https://developer.mozilla.org/zh-CN/docs/Glossary/%E7%AE%80%E5%8D%95%E5%A4%B4%E9%83%A8
預檢請求:https://developer.mozilla.org/zh-CN/docs/Glossary/preflight_request
HTTP Headers:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers
HTTP訪問控制(CORS):https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
postMessage:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
Document.domain:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain