跨域總結

An_an發表於2018-07-31

1.什麼是跨域

同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。同源指:協議、域名、埠號必須一致。

同源策略控制了不同源之間的互動,例如在使用XMLHttpRequest 或 跨域總結 標籤時則會受到同源策略的約束。這些互動通常分為三類:

  • 通常允許跨域寫操作(Cross-origin writes)。例如連結(links),重定向以及表單提交。特定少數的HTTP請求需要新增 preflight
  • 通常允許跨域資源嵌入(Cross-origin embedding)。
  • 通常不允許跨域讀操作(Cross-origin reads)。但常可以通過內嵌資源來巧妙的進行讀取訪問。例如可以讀取嵌入圖片的高度和寬度,呼叫內嵌指令碼的方法,或availability of an embedded resource.

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

  • <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標籤不受跨域限制而形成的一種方案。

// 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]攻擊。

cors

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

<!--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() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。

window.postMessage() 方法被呼叫時,會在所有頁面指令碼執行完畢之後(e.g., 在該方法之後設定的事件、之前設定的timeout 事件,etc.)向目標視窗派發一個MessageEvent訊息。

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

  • otherWindow:指目標視窗,也就是給哪個window發訊息,是 window.frames 屬性的成員或者由 window.open 方法建立的視窗;
  • message 屬性是要傳送的訊息,型別為 String、Object (IE8、9 不支援);
  • targetOrigin:屬性來指定哪些視窗能接收到訊息事件,其值可以是字串"*"(表示無限制)或者一個URI。
  • transfer:是一串和message 同時傳遞的 Transferable 物件. 這些物件的所有權將被轉移給訊息的接收方,而傳送一方將不再保有所有權。

message屬性有:

  • 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");
複製程式碼

相關文章