同源策略/SOP(Same origin policy)是一種約定,由 Netscape 公司 1995 年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,若是缺乏了同源策略,瀏覽器很容易受到 XSS、CSRF 等攻擊。所謂同源是指 "協議 + 域名 + 端口" 三者相同,即使兩個不一樣的域名指向同一個 ip 地址,也非同源。javascript
當協議、域名、端口號,有一個或多個不一樣時,有但願能夠訪問並獲取數據的現象稱爲跨域訪問,同源策略限制下 cookie
、localStorage
、dom
、ajax
、IndexDB
都是不支持跨域的。html
假設 cookie 支持了跨域,http 協議無狀態,當用戶訪問了一個銀行網站登陸後,銀行網站的服務器給返回了一個 sessionId,當經過當前瀏覽器再訪問一個惡意網站,若是 cookie 支持跨域,惡意網站將獲取 sessionId 並訪問銀行網站,出現安全性問題;IndexDB、localStorage 等數據存儲在不一樣域的頁面切換時是獲取不到的;假設 dom 元素能夠跨域,在本身的頁面寫入一個 iframe 內部嵌入的地址是 www.baidu.com,當在百度頁面登陸帳號密碼時就能夠在本身的頁面獲取百度的數據信息,這顯然是不合理的。前端
這就是爲何 cookie
、localStorage
、dom
、ajax
、IndexDB
會受到同源策略會限制,下面還有一點對跨域理解的誤區:vue
誤區:同源策略限制下,訪問不到後臺服務器的數據,或訪問到後臺服務器的數據後沒有返回;
正確:同源策略限制下,能夠訪問到後臺服務器的數據,後臺服務器會正常返回數據,而被瀏覽器給攔截了。java
使用場景:當本身的項目前端資源和後端部署在不一樣的服務器地址上,或者其餘的公司須要訪問本身對外公開的接口,須要實現跨域獲取數據,如百度搜索。node
// 封裝 jsonp 跨域請求的方法 function jsonp({ url, params, cb }) { return new Promise((resolve, reject) => { // 建立一個 script 標籤幫助咱們發送請求 let script = document.createElement("script"); let arr = []; params = { ...params, cb }; // 循環構建鍵值對形式的參數 for (let key in params) { arr.push(`${key}=${params[key]}`); } // 建立全局函數 window[cb] = function(data) { resolve(data); // 在跨域拿到數據之後將 script 標籤銷燬 document.body.removeChild(script); }; // 拼接發送請求的參數並賦值到 src 屬性 script.src = `${url}?${arr.join("&")}`; document.body.appendChild(script); }); } // 調用方法跨域請求百度搜索的接口 json({ url: "https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su", params: { wd: "jsonp" }, cb: "show" }).then(data => { // 打印請求回的數據 console.log(data); });
缺點:webpack
`let script = document.createElement('script'); script.src = "http://192.168.0.57:8080/xss.js"; document.body.appendChild(script);`;
會把別人的腳本引入到本身的頁面中執行,如:彈窗、廣告等,甚至更危險的腳本程序。nginx
跨源資源共享/CORS(Cross-Origin Resource Sharing)是 W3C 的一個工做草案,定義了在必須訪問跨源資源時,瀏覽器與服務器應該如何溝通。CORS 背後的基本思想,就是使用自定義的 HTTP 頭部讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,仍是應該失敗。web
使用場景:多用於開發時,前端與後臺在不一樣的 ip 地址下進行數據訪問。ajax
如今啓動兩個端口號不一樣的服務器,建立跨域條件,服務器(NodeJS)代碼以下:
// 服務器1 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(3000); // 服務器2 const express = require("express"); let app = express(); app.get("/getDate", function(req, res) { res.end("I love you"); }); app.use(express.static(__dirname)); app.listen(4000);
因爲咱們的 NodeJS 服務器使用 express
框架,在咱們的項目根目錄下的命令行中輸入下面代碼進行安裝:
npm install express --save
經過訪問 http://localhost:3000/index.html 獲取 index.html
文件並執行其中的 Ajax
請求 http://localhost:4000/getDate 接口去獲取數據,index.html
文件內容以下:
<!-- 文件:index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CORS 跨域</title> </head> <body> <script> let xhr = new XMLHttpRequest(); // 正常 cookie 是不容許跨域的 document.cookie = 'name=hello'; // cookie 想要實現跨域必須攜帶憑證 xhr.withCredentials = true; // xhr.open('GET', 'http://localhost:4000/getDate', true); xhr.open('PUT', 'http://localhost:4000/getDate', true); // 設置名爲 name 的自定義請求頭 xhr.setRequestHeader('name', 'hello'); xhr.onreadystatechange = function () { if(xhr.readyState === 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { // 打印返回的數據 console.log(xhr.response); // 打印後臺設置的自定義頭信息 console.log(xhr.getResponseHeader('name')); } } } xhr.send(); </script> </body> </html>
上面 index.html
代碼中發送請求訪問不在同源的服務器 2
,此時會在控制檯給出錯誤信息,告訴咱們缺乏了哪些響應頭,咱們對應報錯信息去修改訪問的服務器 2
的代碼,添加對應的響應頭,實現 CORS 跨域。
// 服務器2 const express = require("express"); let app = express(); // 容許訪問域的白名單 let whiteList = ["http://localhost:3000"]; app.use(function(req, res, next) { let origin = req.header.origin; if (whiteList.includes(origin)) { // 設置那個源能夠訪問我,參數爲 * 時,容許任何人訪問,可是不能夠和 cookie 憑證的響應頭共同使用 res.setHeader("Access-Control-Allow-Origin", origin); // 想要獲取 ajax 的頭信息,需設置響應頭 res.setHeader("Access-Control-Allow-Headers", "name"); // 處理複雜請求的頭 res.setHeader("Access-Control-Allow-Methods", "PUT"); // 容許發送 cookie 憑證的響應頭 res.setHeader("Access-Control-Allow-Credentials", true); // 容許前端獲取哪一個頭信息 res.setHeader("Access-Control-Expose-Headers", "name"); // 處理 OPTIONS 預檢的存活時間,單位 s res.setHeader("Access-Control-Max-Age", 5); // 發送 PUT 請求會作一個試探性的請求 OPTIONS,實際上是請求了兩次,當接收的請求爲 OPTIONS 時不作任何處理 if (req.method === "OPTIONS") { res.end(); } } next(); }); app.put("/getDate", function(req, res) { // res.setHeader('name', 'nihao'); // 設置自定義響應頭信息 res.end("I love you"); }); app.get("/getDate", function(req, res) { res.end("I love you"); }); app.use(express.static(__dirname)); app.listen(4000);
postMessage 是 H5 的新 API,跨文檔消息傳送(cross-document messaging),有時候簡稱爲 XMD,指的是在來自不一樣域的頁面間傳遞消息。
調用方式:window.postMessage(message, targetOrigin)
在對應的頁面中用 message 事件接收,事件對象中有 data
、origin
、source
三個重要信息
使用場景:不是使用 Ajax
的數據通訊,更可能是在兩個頁面之間的通訊,在 A
頁面中引入 B
頁面,在 A
、B
兩個頁面之間通訊。
與上面 CORS 相似,咱們要建立跨域場景,搭建兩個端口號不一樣的 Nodejs 服務器,後面相同方式就很少贅述了。
// 服務器1 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(3000); // 服務器2 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(4000);
經過訪問 http://localhost:3000/a.html,在 a.html
中使用 iframe
標籤引入 http://localhost:4000/b.html,在兩個窗口間傳遞數據。
<!-- 文件:a.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 A</title> </head> <body> <iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe> <script> function load() { let frame = document.getElementById('frame'); frame.contentWindow.postMessage('I love you', 'http://localhost:4000'); window.onmessage = function (e) { console.log(e.data); } } </script> </body> </html>
<!-- 文件:b.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 B</title> </head> <body> <script> window.onmessage = function (e) { // 打印來自頁面 A 的消息 console.log(e.data); // 給頁面 A 發送回執 e.source.postMessage('I love you, too', e.origin); } </script> </body> </html>
一樣是頁面之間的通訊,須要藉助 iframe
標籤,A
頁面和 B
頁面是同域的 http://localhost:3000,C
頁面在獨立的域 http://localhost:4000。
// 服務器1 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(3000); // 服務器2 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(4000);
實現思路:在 A
頁面中將 iframe
的 src
指向 C
頁面,在 C
頁面中將屬性值存入 window.name
中,再把 iframe
的 src
換成同域的 B
頁面,在當前的 iframe
的 window
對象中取出 name
的值,訪問 http://localhost:3000/a.html。
<!-- 文件:a.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 A</title> </head> <body> <iframe src="http://localhost:4000/c.html" id="frame" onload="load()"></iframe> <script> // 增長一個標識,第一次觸發 load 時更改地址,更改後再次觸發直接取值 let isFirst = true; function load() { let frame = document.getElementById('frame'); if(isFirst) { frame.src = 'http://localhost:3000/b.html'; isFirst = false; } else { console.log(frame.contentWindow.name); } } </script> </body> </html>
<!-- 文件:c.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 C</title> </head> <body> <script> window.name = 'I love you'; </script> </body> </html>
<br/>
與 window.name
跨域的狀況相同,是不一樣域的頁面間的參數傳遞,須要藉助 iframe
標籤,A
頁面和 B
頁面是同域的 http://localhost:3000,C
頁面是獨立的域 http://localhost:4000。
// 服務器1 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(3000); // 服務器2 const express = require(express); let app = express(); app.use(express.static(__dirname)); app.listen(4000);
實現思路:A
頁面經過 iframe
引入 C
頁面,並給 C
頁面傳一個 hash
值,C
頁面收到 hash
值後建立 iframe
引入 B
頁面,把 hash
值傳給 B
頁面,B
頁面將本身的 hash
值放在 A
頁面的 hash
值中,訪問 http://localhost:3000/a.html。
<!-- 文件:a.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 A</title> </head> <body> <iframe src="http://localhost:4000/c.html#Iloveyou" id="frame"></iframe> <script> // 使用 hashchange 事件接收來自 B 頁面設置給 A 頁面的 hash 值 window.onhashchange = function () { console.log(location.hash); } </script> </body> </html>
<!-- 文件:c.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 C</title> </head> <body> <script> // 打印 A 頁面引入 C 頁面設置的 hash 值 console.log(location.hash); let iframe = document.createElement('iframe'); iframe.src = 'http://localhost:3000/b.html#Iloveyoutoo'; document.body.appendChild(iframe); </script> </body> </html>
<!-- 文件:b.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 B</title> </head> <body> <script> // 將 C 頁面引入 B 頁面設置的 hash 值設置給 A頁面 window.parent.parent.location.hash = location.hash; </script> </body> </html>
<br/>
使用場景:不是萬能的跨域方式,大多使用於同一公司不一樣產品間獲取數據,必須是一級域名和二級域名的關係,如 www.baidu.com 與 video.baidu.com 之間。
const express = require("express"); let app = express(); app.use(express.static(__dirname)); app.listen(3000);
想要模擬使用 document.domain
跨域的場景須要作些小小的準備,到 C:WindowsSystem32driversetc 該路徑下找到 hosts
文件,在最下面建立一個一級域名和一個二級域名。
127.0.0.1 www.domainacross.com
127.0.0.1 sub.domainacross.com
命名是隨意的,只要是符合一級域名與 二級域名的關係便可,而後訪問 http://www.domainacross.com:3000/a.html。
<!-- 文件:a.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面 A</title> </head> <body> <p>我是頁面 A 的內容</p> <iframe src="http://sucess.domainacross.com:3000/b.html" onload="load()" id="frame"></iframe> <script> document.domain = 'domainacross.com'; function load() { console.log(frame.contentWindow.message); } </script> </body> </html>
<!-- 文件:b.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>頁面 B</title> </head> <body> <p>我是 B 頁面的內容</p> <script> document.domain = 'domainacross.com'; var message = 'Hello A'; </script> </body> </html>
WebSocket 沒有跨域限制,高級 API(不兼容),想要兼容低版本瀏覽器,可使用 socket.io
的庫,WebSocket 與 HTTP 內部都是基於 TCP 協議,區別在於 HTTP 是單向的(單雙工),WebSocket 是雙向的(全雙工),協議是 ws://
和 wss://
對應 http://
和 https://
,由於沒有跨域限制,因此使用 file://
協議也能夠進行通訊。
因爲咱們在 NodeJS 服務中使用了 WebSocket,因此須要安裝對應的依賴:
npm install ws --save
<!-- 文件:index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>頁面</title> </head> <body> <script> // 建立 webSocket let socket = new WebSocket('ws://localhost:3000'); // 鏈接上觸發 socket.onopen = function () { socket.send('I love you'); } // 收到消息觸發 socket.onmessage = function (e) { // 打印收到的數據 console.log(e.data); // I love you, too } </script> </body> </html>
const express = require("express"); let app = express(); // 引入 webSocket const WebSocket = require("ws"); // 建立鏈接,端口號與前端相對應 let wss = new WebSocket.Server({ port: 3000 }); // 監聽鏈接 wss.on("connection", function(ws) { // 監聽消息 ws.on("message", function(data) { // 打印消息 console.log(data); // I love you // 發送消息 ws.send("I love you, too"); }); });
nginx
自己就是一個服務器,所以咱們須要去 nginx
官網下載服務環境 http://nginx.org/en/download....。
nginx.exe
啓動(此時能夠經過 http://localhost 訪問 nginx
服務)json
文件夾json
文件夾新建 data.json
文件並寫入內容nginx
根目錄進入 conf
文件夾nginx.conf
進行配置data.json 文件:
{ "name": "nginx" }
nginx.conf 文件:
server { . . . location ~.*\.json { root json; add_header "Access-Control-Allow-Origin" "*"; } . . . }
含義:
json
文件夾;*
爲容許任何訪問。在 nginx
根目錄啓動 cmd
命令行(windows 系統必須使用 cmd
命令行)執行下面代碼重啓 nginx
。
nginx -s reload
不跨域訪問:http://localhost/data.json
跨域訪問時須要建立跨域條件代碼以下:
// 服務器 const express = require("express"); let app = express(); app.use(express.static(__dirname)); app.listen(3000);
跨域訪問:http://localhost:3000/index.html
<!-- 文件:index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>nginx跨域</title> </head> <body> <script> let xhr = new XMLHttpRequest(); xhr.open('GET', 'http://localhost/data.json', true); xhr.onreadystatechange = function () { if(xhr.readyState === 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { console.log(xhr.response); } } } xhr.send(); </script> </body> </html>
<br/>
NodeJS 中間件 http-proxy-middleware
實現跨域代理,原理大體與 nginx
相同,都是經過啓一個代理服務器,實現數據的轉發,也能夠經過設置 cookieDomainRewrite
參數修改響應頭中 cookie
中的域名,實現當前域的 cookie
寫入,方便接口登陸認證。
<!-- 文件:index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>proxy 跨域</title> </head> <body> <script> var xhr = new XMLHttpRequest(); // 前端開關:瀏覽器是否讀寫 cookie xhr.withCredentials = true; // 訪問 http-proxy-middleware 代理服務器 xhr.open('get', 'http://www.proxy1.com:3000/login?user=admin', true); xhr.send(); </script> </body> </html>
中間代理服務中使用了 http-proxy-middleware
中間件,所以須要提早下載:
npm install http-proxy-middleware --save-dev
// 中間代理服務器 const express = require("express"); let proxy = require("http-proxy-middleware"); let app = express(); app.use( "/", proxy({ // 代理跨域目標接口 target: "http://www.proxy2.com:8080", changeOrigin: true, // 修改響應頭信息,實現跨域並容許帶 cookie onProxyRes: function(proxyRes, req, res) { res.header("Access-Control-Allow-Origin", "http://www.proxy1.com"); res.header("Access-Control-Allow-Credentials", "true"); }, // 修改響應信息中的 cookie 域名 cookieDomainRewrite: "www.proxy1.com" // 能夠爲 false,表示不修改 }) ); app.listen(3000);
// 服務器 const http = require("http"); const qs = require("querystring"); const server = http.createServer(); server.on("request", function(req, res) { let params = qs.parse(req.url.substring(2)); // 向前臺寫 cookie res.writeHead(200, { "Set-Cookie": "l=a123456;Path=/;Domain=www.proxy2.com;HttpOnly" // HttpOnly:腳本沒法讀取 }); res.write(JSON.stringify(params)); res.end(); }); server.listen("8080");
利用 node + webpack + webpack-dev-server 代理接口跨域。在開發環境下,因爲 Vue
渲染服務和接口代理服務都是 webpack-dev-server
,因此頁面與代理接口之間再也不跨域,無須設置 Headers
跨域信息了。
// 導出服務器配置 module.exports = { entry: {}, module: {}, ... devServer: { historyApiFallback: true, proxy: [{ context: '/login', target: 'http://www.proxy2.com:8080', // 代理跨域目標接口 changeOrigin: true, secure: false, // 當代理某些 https 服務報錯時用 cookieDomainRewrite: 'www.domain1.com' // 能夠爲 false,表示不修改 }], noInfo: true } }
本篇文章在於幫助咱們理解跨域,以及不一樣跨域方式的基本原理,在公司的項目比較多,多個域使用同一個服務器或者數據,以及在開發環境時,跨域的狀況基本沒法避免,通常會有各類各樣形式的跨域解決方案,但其根本原理基本都在上面的跨域方式當中方式,咱們能夠根據開發場景不一樣,選擇最合適的跨域解決方案。