咱們試想一下如下幾種狀況:javascript
爲了解決不一樣域名相互訪問數據致使的不安全問題,Netscape提出的一個著名的安全策略——同源策略,它是指同一個「源頭」的數據能夠自由訪問,但不一樣源的數據相互之間都不能訪問。html
很明顯,上述第1個和第3個例子中,不一樣的天貓商店和 qq 空間屬於同源,能夠共享登陸信息。qq 爲了區別不一樣的 qq 的登陸信息,從新打開了一個窗口,由於瀏覽器的不一樣窗口是不能共享信息的。而第2個例子中的支付寶、網銀、不知名網站之間是非同源的,因此彼此之間沒法訪問信息,若是你執意想請求數據,會提示異常:前端
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
那麼什麼是同源的請求呢?同源請求要求被請求資源頁面和發出請求頁面知足3個相同:java
協議相同
host相同
端口相同
簡單理解一下:node
/*如下兩個數據非同源,由於協議不一樣*/ http://www.abc123.com.cn/item/a.js https://www.abc123.com.cn/item/a.js /*如下兩個數據非同源,由於域名不一樣*/ http://www.abc123.com.cn/item/a.js http://www.abc123.com/item/a.js /*如下兩個數據非同源,由於主機名不一樣*/ http://www.abc123.com.cn/item/a.js http://item.abc123.com.cn/item/a.js /*如下兩個數據非同源,由於協議不一樣*/ http://www.abc123.com.cn/item/a.js http://www.abc123.com.cn:8080/item/a.js /* 如下兩個數據非同源,域名和 ip 視爲不一樣源 * 這裏應注意,ip和域名替換同樣不是同源的 * 假設www.abc123.com.cn解析後的 ip 是 195.155.200.134 */ http://www.abc123.com.cn/ http://195.155.200.134/ /*如下兩個數據同源*/ /* 這個是同源的*/ http://www.abc123.com.cn/source/a.html http://www.abc123.com.cn/item/b.js
http 請求知足一下條件時稱爲簡單請求,不然是非簡單請求:webpack
HTTP的頭信息不超出如下幾種字段:web
application/x-www-form-urlencoded
, multipart/form-data
, text/plain
非簡單請求在發送以前會發送一次 OPTION 預請求,若是在跨域操做遇到返回 405(Method Not Allowed) 錯誤,須要服務端容許 OPTION 請求。shell
適用條件:請求的 GET 接口須要支持 jsonp 訪問
這裏須要強調的是,jsonp 不屬於 Ajax 的部分,它只是把 url 放入 script 標籤中實現的數據傳輸,不受同源策略限制。因爲通常庫也會把它和 Ajax 封裝在一塊兒,因爲其和 Ajax 根部不是一回事,因此這裏不討論。下面是一個 jsonp 的例子:express
window.jsonpCallback = console.log; var JSONP = document.createElement("script"); JSONP.src = "http://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13122222222&t=" + Math.random() + "&callback=jsonpCallback";; document.body.appendChild(JSONP);
後端支持jsonp方式(Nodejs)npm
var querystring = require('querystring'); var http = require('http'); var server = http.createServer(); server.on('request', function(req, res) { var params = qs.parse(req.url.split('?')[1]); var fn = params.callback; // jsonp返回設置 res.writeHead(200, { 'Content-Type': 'text/javascript' }); res.write(fn + '(' + JSON.stringify(params) + ')'); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...');
document.domain
適用條件: host 中僅服務器不一樣的狀況,域名自己應該相同
www.dom.com
和 w1.dom.com
須要同源才能訪問,能夠將 document.domain 設置爲 dom.com
解決該問題
document.domain = 'dom.com';
例如,我想開發一個瀏覽器插件,發現騰訊視頻頁有個 iframe 其自己的跨域的,沒法獲取其 iframe 的 DOM 對象。但域名部分相同,能夠經過該方法解決.
注:若是你想設置它爲徹底不一樣的域名,那確定會報同源錯誤的,注意使用範圍!
適用條件: host 中僅服務器不一樣的狀況,域名自己應該相同
有了上面的例子就不難理解這個方法了,嚴格來講這不是一個新的方法,而是上一個方法的延伸。經過設置document.domain
, 使同一個域名下不一樣服務器名的頁面能夠訪問數據,但值得注意的是:這個數據訪問不是相互的,外部頁面能夠訪問 iframe 內部的數據,但 iframe 沒法不能訪問外部的數據。
適用條件:iframe 和其宿主頁面通訊
一個完成的 url 中 # 及後面的部分爲 hash, 能夠經過修改這個部分完成iframe 的和宿主直接的數據傳遞,下面演示一下 iframe 頁面(B.html)像宿主(A.html)傳數據, 反之同理:
// A.html data = ['book', 'map', 'shelf', 'knife']; setTimeout(() => { location.hash = window.encodeURIComponent(data.join('/')); }, 1000); // B.html window.parent.onhashchange = function (e) { var data = window.decodeURIComponent(e.newURL.split('#')[1]).split('/'); console.log(data); // ["book", "map", "shelf", "knife"] }
*注意反向傳遞數據時應該使用 window.parent.location.hash
window.name
適用條件:宿主頁面和 iframe 之間通訊
window對象有個name屬性,該屬性有個特徵:即在 window 的生命週期內,窗口載入的全部的頁面 (iframe) 都是共享一個 window.name
的,每一個頁面對 window.name
都有讀寫的權限,window.name
是持久存在一個窗口載入過的全部頁面中的,並不會因新頁面的載入而進行重置。
這樣在 window 中編輯 window.name 就能夠在 iframe 中獲得,但這個過程缺少監聽,宿主頁面(A.html)和 iframe 頁面(B.html)相互並不知道對方在何時修改該值:
// A.html setTimeout(() => { window.parent.name = "what!"; }, 2000); // B.html setTimeout(() => { console.log(window.name); // what! }, 2500);
postMessage
適用條件:postMessage 是 H5 提出的一個消息互通的機制,解決 iframe 不能消息互通的問題,也能夠跨 window 通訊,語法以下:
// 在 www.siteA.com 中發出消息 // @message{any} 要發送的數據(注意:老版本瀏覽器只支持字符串類型) // @targetOrigin{string} 規定接收數據的域,只有其指定的域才能收到消息,若是爲"*"則沒用域的限制 // transfer{any} 與 message 一同發送並轉移全部權 window.postMessage(message, targetOrigin, [transfer]); // 在另外一個頁面接受參數 window.onmessage = console.log;
這裏暫不談論第三個參數,由於你可能一生也用不到它。而 targetOrigin 最好不要使用 "*",除非你想讓全部頁面都收到你的消息。
一種你會用到的場景(iframe):
<!-- www.siteA.com/index.html --> <script> window.addEventListener('message', function(e){ console.log('Get message: "' + e.data.title + '" from ' + e.origin); // 'Get message: "Saying hello to siteA!" from http://www.siteB.com' }); </script> <iframe src="http://www.siteB.com"></iframe> <!-- www.siteB.com/index.html --> <script> function sendMessage(){ window.postMessage({title: 'Saying hello to siteA!'}, 'http://www.siteA.com'); } setTimeout(sendMessage, 2000); </script>
這一種僅僅是沒有了iframe,當你在同一個瀏覽器窗口同時打開 www.siteA.com
和 www.siteB.com
兩個標籤時也能夠這樣用
<!-- www.siteA.com/index.html --> <script> window.addEventListener('message', function(e){ console.log('Get message: "' + e.data.title + '" from ' + e.origin); // 'Get message: "Saying hello to siteA!" from http://www.siteB.com' }); </script> <!-- www.siteB.com/index.html --> <script> function sendMessage(){ window.postMessage({title: 'Saying hello to siteA!'}, 'http://www.siteA.com'); } setTimeout(sendMessage, 2000); </script>
頁面須要訪問一些跨域接口,因爲代理的存在,在服務器看來請求是不跨域,因此使用各類請求。但須要注意 http 到 https 的兼容問題。
好比當我在一些在線平臺開發網站後獲得一個頁面 www.site-A.com
, 而這個頁面須要請求我本身的數據服務器data.site-B.com
上的數據, 這樣一樣會產生跨域問題,可是www.site-A.com
這個頁面是掛在第三方服務器上的,解決這個問題能夠採用代理服務器的方法:
var express = require('express'); var request = require('request'); var app = express(); app.use('/api', function(req, res) { var url = 'http://data.site-B.com/api2' + req.url; req.pipe(request(url)).pipe(res); }); app.use('/', function(req, res) { var url = 'http://data.site-C.com'; req.pipe(request(url)).pipe(res); });
固然還須要同時配置一個 host:
127.0.0.1 local.www.site-B.com
而後訪問 local.www.site-B.com 就 OK 了。
適用條件:CORS 須要服務端支持,且存在必定的兼容性問題(現在你已經能夠不考慮,但必要時不要忘了這個'bug')。其經過添加 http 頭關鍵字實現跨域可訪問,包括以下頭內容:
# www.siteA.com/api 返回相應須要具備以下 http 頭字段 Access-Control-Allow-Origin: 'http://www.siteB.com' # 指定域能夠請求,通配符'*'(必須) Access-Control-Allow-Methods: 'GET,PUT,POST,DELETE' # 指定容許的跨域請求方式(必須) Access-Control-Allow-Headers: 'Content-Type' # 請求中必須包含的 http 頭字段 Access-Control-Allow-Credentials: true # 配合請求中的 withCredentials 頭進行請求驗證
經過 express 實現也很簡單,在註冊路由以前添加:
var cors = require('cors'); // 經過 npm 安裝 app.use(cors());
固然你也能夠自定義一箇中間件:
// 自定義中間件 var cors = function (req, res, next) { // 自定義設置跨域須要的響應頭。 res.header('Access-Control-Allow-Origin', 'http://www.siteB.com'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); next(); }; app.use(cors); // 運用跨域的中間件
ws 協議是 H5 中的 web 全雙工通訊解決方案,常規 http 屬於請求相應的過程,在客戶端沒有請求的狀況下,服務端沒法給客戶端主動推送數據,ws 協議解決了這個問題,但處於安全考慮,其一樣有同源策略的限制。
*這裏不討論經過長鏈接和服務端掛起請求等方法推送數據,本文只討論跨域。
下面舉個例子(依賴socket.io.js):
// 前端部分 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.getElementById('input').onkeyup = function(e) { if(!e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 13) socket.send(this.value); }; // 後端部分(node.js) var http = require('http'); var socket = require('socket.io'); // 啓http服務 var server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-type': 'text/html' }); res.end(); }); server.listen('8080'); console.log('Server is running at port 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.'); }); });
HTML 中 <img>
, <video>
和 <script>
具備 crossorigin 屬性。添加屬性會使相應添加 CORS 相關 http 頭(須要服務器支持)。同時,其還有如下可能的取值:
當只寫了 crossorigin 屬性沒有指定值時,其默認值爲 "anonymous"。即如下兩行代碼等價:
<scirpt src="a.com/vendor.js" corssorigin></script> <scirpt src="a.com/vendor.js" corssorigin="anonymous"></script>
方法 | 使用條件 | 使用條件是否與後端交互 | 優勢 | 缺點 | |
---|---|---|---|---|---|
JSONP | 服務端支持 jsonp 請求 | 是 | 兼容全部瀏覽器 | 只支持 GET 請求,只能和服務端通訊 | |
CORS | 服務器相應須要相關投資端支持 | 是 | 方便的錯誤處理,支持全部http請求類型 | 存在瀏覽器兼容性問題(現在能夠忽略了) | |
document.domain |
僅須要跨子域發起請求 | 是 | 使用便捷,沒有兼容問題 | 對於徹底不一樣的域名沒法使用 | |
postMessage |
瀏覽器不一樣 window 間通訊、 iframe 和其宿主通訊 | 否 | 支持瀏覽器頁面間或頁面和 iframe 間同行 | 須要瀏覽器兼容 H5 接口 | |
window.name |
iframe 和其宿主通訊 | 否 | 簡單易操做 | 數據暴露在全局不安全 | |
location.hash |
iframe 和其宿主通訊 | 否 | 簡單易操做 | 數據在 url 中不安全而且有長度限制 | |
反向代理 | - | 是 | 任何狀況均可用 | 使用比較麻煩,須要本身創建服務 |
添加 webpack 配置以下:
const config = { // ... devServer: { // ... proxy: { '/api': { target: 'https://data.site-B.com/api2', changeOrigin: true, // 容許跨域 secure: false // 容許訪問 https }, '/': { target: 'https://data.site-C.com', changeOrigin: true, secure: false }, } } }; module.exports = config;
location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods: GET,PUT,POST,DELETE; }
server { listen 7001; server_name www.domain1.com; location / { proxy_pass http://www.B.com:7001; #反向代理 } }