跨域是指不一樣協議、域名、端口下訪問js腳本。而當遇到跨域時,因爲瀏覽器中同源策略的安全限制,致使不能正常執行,報相似如下的錯誤:javascript
同源策略是指限制了腳本與不一樣源的資源交互,而當中的源是以協議、域名和端口區分,如下狀況爲不一樣源:html
http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不一樣源(域名不一樣)
http://v2.www.example.com/dir/other.html:不一樣源(域名不一樣)
http://www.example.com:81/dir/other.html:不一樣源(端口不一樣)
複製代碼
(1) Cookie、LocalStorage 和 IndexDB 沒法讀取。
(2) DOM 沒法得到。
(3) AJAX 請求不能發送。
複製代碼
這裏只總結關於AJAX方面的跨域問題。同源政策規定,AJAX請求只能發給同源的網址,不然就報錯。而解決方案通常有JSONP
、CORS
和WebSocket
。前端
JSONP是服務器與客戶端跨源通訊的經常使用方法,這裏咱們寫一個例子去探究JSONP
的原理。java
首先,網頁經過添加一個<script>
元素,向服務器請求JSON數據,這種作法不受同源政策限制。jquery
render.js:
const express = require('express');
const app = express();
app.get('/jsonp', (req, res) => {
const viewPath = `${__dirname}/jsonp.html`;
res.setHeader('Content-Type', 'text/html');
res.sendFile(viewPath);
});
app.listen(8081);
console.log(`listen 8081....`);
複製代碼
jsonp.html:
<html>
<body>
<h1>jsonp test</h1>
</body>
<script>
function addScriptsTag (src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
console.log('onload');
addScriptsTag('http://127.0.0.1:8080/ip?callback=foo');
}
function foo (data) {
console.log(`Your public IP address is:${data.ip}`);
}
</script>
</html>
複製代碼
服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來。ios
const express = require('express');
const app = express();
app.get('/ip', (req, res) => {
const callback = req.query.callback;
res.send(`/**/ typeof ${callback} === 'function' && ${callback} ({"ip":"127.0.0.2"});`);
// res.jsonp({ ip: '127.0.0.1' });// express自帶jsonp更方便
});
app.listen(8080);
console.log(`listen 8080....`);
複製代碼
最後前端收到返回的數據以下所示,瀏覽器就會執行該腳本,調用前面定義好的回調函數foo
。express
/**/ typeof foo === 'function' && foo ({"ip":"127.0.0.2"});
複製代碼
能夠自行封裝方法或者找第三方庫去實現JSONP請求,好比jquery的jsonp方法。json
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。axios
CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。跨域
瀏覽器將CORS請求分紅兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
只要同時知足如下兩大條件,就屬於簡單請求。
(1) 請求方法是如下三種方法之一:
- HEAD
- GET
- POST
(2)HTTP的頭信息不超出如下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
複製代碼
凡是不一樣時知足上面兩個條件,就屬於非簡單請求
輸入127.0.0.1:8081/cors
,瀏覽器正常進行一次簡單的AJAX請求(使用axios):
render.js
const express = require('express');
const app = express();
app.get('/cors', (req, res) => {
const viewPath = `${__dirname}/cros.html`;
res.setHeader('Content-Type', 'text/html');
res.sendFile(viewPath);
});
app.listen(8081);
console.log(`listen 8081....`);
複製代碼
cros.html:
<html>
<body>
<h1>cors test</h1>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios({
method: 'post',
baseURL: 'http://127.0.0.1:8080/',
url: '/user',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},// 默認是applictaion/json,爲非簡單請求,因此爲了測試需自定義
data: {
firstName: 'Fred',
lastName: 'Flintstone'
},
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
</script>
</body>
</html>
複製代碼
服務器經過中間件形式加上頭部信息:
const express = require('express');
const app = express();
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:8081");
next();
});
app.post('/user', (req, res) => {
res.send('user ok');
});
app.listen(8080);
console.log(`listen 8080....`);
複製代碼
這時候CORS就成功了,通訊過程的頭部信息:
請求頭:
POST http://127.0.0.1:8080/user HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 44
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8081
...
{"firstName":"Fred","lastName":"Flintstone"}
複製代碼
響應頭:
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://127.0.0.1:8081
...
user ok
複製代碼
其中起關鍵做用的是Origin
和Access-Control-Allow-Origin
,Origin
瀏覽器請求時會帶上,指明請求的來源。而響應頭的Access-Control-Allow-Origin
指明可以跨域請求資源的容許網址,多個用逗號隔開,若是寫*號代表任意網址均可以。
當請求爲非簡單請求時,一次請求會分兩次請求,分別是預檢請求和正常的請求。
咱們如今把上面AJAX請求方法變爲PUT
,
請求就會先發一次預檢請求。請求頭以下:
OPTIONS http://127.0.0.1:8080/user HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Access-Control-Request-Method: PUT
Origin: http://127.0.0.1:8081
...
複製代碼
預檢請求的方法爲OPTIONS
,代表請求時用來詢問的。Origin
依然代表請求來源。而新增的請求字段Access-Control-Request-Method
代表接下來的CORS請求會到什麼方法,可否使用。
這時服務器經過設置Access-Control-Allow-Methods
來規定服務器支持的CORS請求方法:
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE");
複製代碼
正確返回後,響應頭以下:
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://127.0.0.1:8081
Access-Control-Allow-Methods: PUT,POST,GET,DELETE
Allow: PUT
...
複製代碼
正常請求的請求頭:
PUT http://127.0.0.1:8080/user HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 44
Accept: application/json, text/plain, */*
Origin: http://127.0.0.1:8081
...
{"firstName":"Fred","lastName":"Flintstone"}
複製代碼
迴應:
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://127.0.0.1:8081
Access-Control-Allow-Methods: PUT,POST,GET,DELETE
...
user ok
複製代碼
和簡單請求同樣,請求寫到Origin
,回包攜帶Access-Control-Allow-Origin
客戶端,XMLHttpRequest
對象開啓withCredentials
屬性。
JS寫法:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
複製代碼
axios開啓選項便可:
withCredentials: true,
複製代碼
服務端贊成容許發送Cookie,經過設置頭部:
res.header("Access-Control-Allow-Credentials",true);
複製代碼
Access-Control-Request-Headers
:該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,好比但願請求時使用application/json
格式時,你就須要在服務器設置res.header('Access-Control-Allow-Headers', 'Content-Type');
來支持自定義內容格式。
Access-Control-Max-Age
: 該字段可選,用來指定本次預檢請求的有效期,單位爲秒
參考下面阮一峯老師的總結。
JSONP只支持GET請求方式,客戶端和服務器經過第三庫來書寫也很方便,服務器改動也不多。CORS支持幾乎全部的請求方式,服務器支持只需設置一些頭部信息,而客戶端支持發送Cookie須要開啓一下選項(不多改動)。WebSocket是更適用於實時性、頻繁的請求。因此CORS更通用,開發時也更多使用這種方式。