跨域總結

1.什麼是跨域

同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。同源指:協議、域名、端口號必須一致。html

同源策略控制了不一樣源之間的交互,例如在使用XMLHttpRequest 或 標籤時則會受到同源策略的約束。這些交互一般分爲三類:前端

  • 一般容許跨域寫操做(Cross-origin writes)。例如連接(links),重定向以及表單提交。特定少數的HTTP請求須要添加 preflight
  • 一般容許跨域資源嵌入(Cross-origin embedding)。
  • 一般不容許跨域讀操做(Cross-origin reads)。但常能夠經過內嵌資源來巧妙的進行讀取訪問。例如能夠讀取嵌入圖片的高度和寬度,調用內嵌腳本的方法,或availability of an embedded resource.

下面爲容許跨域資源嵌入的示例,即一些不受同源策略影響的標籤示例:vue

  • <script src="..."></script>標籤嵌入跨域腳本。語法錯誤信息只能在同源腳本中捕捉到。
  • <link rel="stylesheet" href="...">標籤嵌入CSS。因爲CSS的鬆散的語法規則,CSS的跨域須要一個設置正確的Content-Type消息頭。不一樣瀏覽器有不一樣的限制: IE, Firefox, Chrome, SafariOpera
  • <img>嵌入圖片。支持的圖片格式包括PNG,JPEG,GIF,BMP,SVG
  • <video> <audio>嵌入多媒體資源。
  • <object>, <embed> <applet>的插件。
  • @font-face引入的字體。一些瀏覽器容許跨域字體( cross-origin fonts),一些須要同源字體(same-origin fonts)。
  • <frame><iframe>載入的任何資源。站點可使用X-Frame-Options消息頭來阻止這種形式的跨域交互。

2.跨域的解決方案

jsonp

利用script標籤不受跨域限制而造成的一種方案。node

// index.html
function jsonp({url, param, cb}){
    return new Promise((resolve, reject)=>{
        let script = document.createElement('script')
        window[cb] = function(data){
            resolve(data);
            document.body.removeChild(script)
        }
        params = {...params, cb}
        let arrs = [];
        for(let key in params){
            arrs.push(`${key}=${params[key]}`)
        }
        script.src = `${url}?${arrs.join('&')}`
        document.body.appendChild(script)
    })
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {wd: 'haoxl'},
    cb: 'show'
}).then(data=>{
    console.log(data)
})
複製代碼
//server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res){
    let {wd,cb} = req.query
    console.log(wd)
    res.end(`${cb}('hello')`)
})
app.listen(3000)
複製代碼

缺點:只支持get請求,不支持post、put、delete等;不安全,容易受[xss][18]攻擊。webpack

cors

跨域資源共享標準新增了一組 HTTP 首部字段,容許服務器聲明哪些源站有權限訪問哪些資源。另外,規範要求,對那些可能對服務器數據產生反作用的 HTTP 請求方法(特別是 GET 之外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否容許該跨域請求。服務器確認容許以後,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也能夠通知客戶端,是否須要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關數據)。nginx

<!--index.html-->
<body>
    Nice to meet you
</body> 
複製代碼
<script>
let xhr = new XMLHttpRequest;
// 強制前端設置必須帶上請示頭cookie
document.cookie = 'name=haoxl'
xhr.withCredentials = true
xhr.open('GET','http://localhost:4000/getData', true);
// 設置自定義請求頭
xhr.setRequestHeader('name','haoxl')
xhr.onreadystatechange = function(){
    if(xhr.readyState === 4){
        if(xhr.status>=200 && xhr.status < 300 || xhr.status === 304){
            console.log(xhr.response);
            //獲取後臺傳來的已改變name值的請示頭
            console.log(xhr.getResponseHeader('name'));
        }
    }
}
xhr.send()
</script>
複製代碼
// server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
複製代碼
// server2.js
let express = require('express');
let app = express();
let whiteList = ['http://localhost:3000']
app.use(function(req, res, next){
    let origin = req.headers.origin;
    if(whiteList.includes(origin)){
        //設置那個源能夠訪問我,參數爲 * 時,容許任何人訪問,可是不能夠和 cookie 憑證的響應頭共同使用
        res.setHeader('Access-Control-Allow-Origin', origin);
        //容許帶有name的請求頭的能夠訪問
        res.setHeader('Access-Control-Allow-Headers','name');
        // 設置哪些請求方法可訪問
        res.setHeader('Access-Control-Allow-Methods', 'PUT');
        // 設置帶cookie請求時容許訪問
        res.setHeader('Access-Control-Allow-Credentials', true);
        // 後臺改了前端傳的name請示頭後,再傳回去時瀏覽器會認爲不安全,因此要設置下面這個 
        res.setHeader('Access-Control-Expose-Headers','name');
        // 預檢的存活時間-options請示
        res.setHeader('Access-Control-Max-Age',3)
        // 設置當預請求發來請求時,不作任何處理
        if(req.method === 'OPTIONS'){
            res.end();//OPTIONS請示不作任何處理
        }
    }
    next();
});

app.put('/getData', function(req, res){
    console.log(req.headers)
    res.setHeader('name','hello');
    res.end('hello world');
}

app.get('/getData', function(){
    res.end('Nice to meet you')
})
app.use(express.static(__dirname));
app.listen(3000)
複製代碼

postMessage

對於兩個不一樣頁面的腳本,只有當執行它們的頁面位於具備相同的協議(一般爲https),端口號(443爲https的默認值),以及主機 (兩個頁面的模數 Document.domain設置爲相同的值) 時,這兩個腳本才能相互通訊。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。web

window.postMessage() 方法被調用時,會在全部頁面腳本執行完畢以後(e.g., 在該方法以後設置的事件、以前設置的timeout 事件,etc.)向目標窗口派發一個MessageEvent消息。express

語法:otherWindow.postMessage(message, targetOrigin, [transfer]);npm

  • otherWindow:指目標窗口,也就是給哪一個window發消息,是 window.frames 屬性的成員或者由 window.open 方法建立的窗口;
  • message 屬性是要發送的消息,類型爲 String、Object (IE八、9 不支持);
  • targetOrigin:屬性來指定哪些窗口能接收到消息事件,其值能夠是字符串"*"(表示無限制)或者一個URI。
  • transfer:是一串和message 同時傳遞的 Transferable 對象. 這些對象的全部權將被轉移給消息的接收方,而發送一方將再也不保有全部權。

message屬性有:json

  • data 屬性爲 window.postMessage 的第一個參數;
  • origin 屬性表示調用window.postMessage() 方法時調用頁面的當前狀態;
  • source 屬性記錄調用 window.postMessage() 方法的窗口信息;

案例:a.html 給b.html發消息

// a.html
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
<script>
function load(params){
    let frame = document.getElementById('frame');
    //獲取iframe中的窗口,給iframe裏嵌入的window發消息
    frame.contentWindow.postMessage('hello','http://localhost:4000')
    // 接收b.html回過來的消息
    window.onmessage = function(e){
        console.log(e.data)
    }
}
</script>
複製代碼
// b.html
<script>
//監聽a.html發來的消息
window.onmessage = function(e){
    console.log(e.data)
    //給發送源回消息
    e.source.postMessage('nice to meet you',e.origin)
}
</script>
複製代碼

window.name

頁面可能會因某些限制而改變他的源。腳本能夠將 document.domain 的值設置爲其當前域或其當前域的超級域。若是將其設置爲其當前域的超級域,則較短的域將用於後續源檢查。

a和b是同域的http://localhost:3000, c是獨立的http://localhost:4000。 a經過iframe引入c,c把值放到window.name,再把它的src指向和a同域的b,而後在iframe所在的窗口中便可取出name的值。

// a.html
<iframe src="http://localhost:4000/c.html" onload="load()"></iframe>
<script>
let first = true
function load(){
    if(first){
        let iframe = document.getElementById('iframe');
        // 將a中的iframe再指向b
        iframe.src='http://localhost:3000/b.html';
        first = false;
    }else{
        //在b中則可獲得c給窗口發的消息
        console.log(iframe.contentWindow.name);
    }
}
</script>
複製代碼
// c.html
<script>
window.name = 'nice to meet you'
</script>
複製代碼
//server.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000);
複製代碼

location.hash

window.location 只讀屬性,返回一個Location對象,其中包含有關文檔當前位置的信息。**window.location : 全部字母必須小寫!**只要賦給 location 對象一個新值,文檔就會使用新的 URL 加載,就好像使用修改後的 URL 調用了window.location.assign() 同樣。須要注意的是,安全設置,如 CORS(跨域資源共享),可能會限制實際加載新頁面。

案例:a、b同域,c單獨一個域。a如今想訪問c:a經過iframe給c傳一個hash值,c收到hash值後再建立一個iframe把值經過hash傳遞給b,b將hash結果放到a的hash值中。

// a.html
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
//接收b傳來的hash值
window.onhashchange = function(){
    console.log(location.hash)
}
</script>
複製代碼
// c.html
//接收a傳來的hash值
console.log(location.hash)
//建立一個iframe,把回覆的消息傳給b
let iframe = document.createElement('iframe');
iframe.src='http://localhost:3000/b.html#idontloveyou';
document.body.appendChild(iframe);
複製代碼
//b.html
<script>
//a.html引的c, c又引的b,因此b.parent.parent便是a
window.parent.parent.location.hash = location.hash
</script>
複製代碼

window.domain

window.domain:獲取/設置當前文檔的原始域部分。 案例:解決一級域與二級域之間通訊。 模擬時須要建立兩個不一樣域的域名用來測試,打開C:\Windows\System32\drivers\etc 該路徑下找到 hosts 文件,在最下面建立一個一級域名和一個二級域名。改成:

127.0.0.1   www.haoxl.com
127.0.0.1   test.haoxl.com
複製代碼

預設a.html = www.haoxl.com, b.html = test.haoxl.com

// a.html
<iframe src="http://test.haoxl.com" onload="load()"></iframe>
<script>
function load(){
    //告訴頁面它的主域名,要與b.html的主域名相同,這樣纔可在a中訪問b的值
    document.domain = 'haoxl.com'
    function load(){
        // 在a頁面引入b頁面後,直接經過下面方式獲取b中的值
        console.log(frame.contentWindow.a);
    }
}
</script>
複製代碼
// b.html
document.domain = 'haoxl.com'
var a = 'hello world'
複製代碼

websocket

WebSocket對象提供了用於建立和管理 WebSocket 鏈接,以及能夠經過該鏈接發送和接收數據的 API。它是基於TCP的全雙工通訊,即服務端和客戶端能夠雙向進行通信,而且容許跨域通信。基本協議有ws://(非加密)和wss://(加密)

//socket.html
let socket = new WebSocket('ws://localhost:3000');
// 給服務器發消息
socket.onopen = function() {
    socket.send('hello server')
}
// 接收服務器回覆的消息
socket.onmessage = function(e) {
    console.log(e.data)
}

// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//npm i ws
// 設置服務器域爲3000端口
let wss = new WebSocket.Server({port:3000});
//鏈接
wss.on('connection', function(ws){
    // 接收客戶端傳來的消息
    ws.on('message', function(data){
        console.log(data);
        // 服務端回覆消息
        ws.send('hello client')
    })
})
複製代碼

Nginx

Nginx (engine x) 是一個高性能的HTTP反向代理服務器,也是一個IMAP/POP3/SMTP服務器。

案例:在nginx根目錄下建立json/a.json,裏面隨便放些內容

// client.html
let xhr = new XMLHttpRequest;
xhr.open('get', 'http://localhost/a.json', true);
xhr.onreadystatechange = function() {
    if(xhr.readyState === 4){
        if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304 ){
            console.log(xhr.response);
        }
    }
}
複製代碼
// server.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
複製代碼
// nginx.conf
location / {// 表明輸入/時默認去打開root目錄下的html文件夾
    root html;
    index index.html index.htm;
}
location ~.*\.json{//表明輸入任意.json後去打開json文件夾
    root json;
    add_header "Access-Control-Allow-Origin" "*";
}
複製代碼

http-proxy-middleware

NodeJS 中間件 http-proxy-middleware 實現跨域代理,原理大體與 nginx 相同,都是經過啓一個代理服務器,實現數據的轉發,也能夠經過設置 cookieDomainRewrite 參數修改響應頭中 cookie 中的域名,實現當前域的 cookie 寫入,方便接口登陸認證。

  • vue框架:利用 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
    }
}
複製代碼
  • 非vue框架的跨域(2 次跨域)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>nginx跨域</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>
複製代碼
// 中間代理服務器
var express = require("express");
var proxy = require("http-proxy-middleware");
var 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);
複製代碼
// 服務器
var http = require("http");
var server = http.createServer();
var qs = require("querystring");

server.on("request", function(req, res) {
    var 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");
複製代碼
相關文章
相關標籤/搜索