跨域的 N 種實現方式

跨域的 N 種實現方式

跨域, 什麼是跨域?跨域有什麼好處?跨域有什麼很差?怎麼實現跨域?javascript

什麼是跨域

只要協議、域名、端口有任何一個不一樣,都被看成是不一樣的域,之間的請求就是跨域操做. 對於端口和協議的不一樣,只能經過後臺來解決。php

跨域會有什麼好處?

防止CSRF攻擊、同源策略:隔離潛在惡意文件的重要安全機制html

補充知識:java

什麼是CSRF攻擊?node

CSRF(Cross-site request forgery 跨站請求僞造,也被稱爲「One Click Attack」或者Session Riding,一般縮寫爲 CSRF 或者 XSRF,是一種對網站的惡意利用。儘管聽起來像跨站腳本(XSS),但它與 XSS 很是不一樣,而且攻擊方式幾乎相左。XSS 利用站點內的信任用戶,而CSRF則經過假裝來自受信任用戶的請求來利用受信任的網站。與 XSS 攻擊相比,CSR F攻擊每每不大流行(所以對其進行防範的資源也至關稀少)和難以防範,因此被認爲比XSS更具危險性。 爲何會出現CSRF攻擊?jquery

舉例說明ios

好比說有兩個網站 A 和 B。你是 A 網站的管理員,你在 A 網站有一個權限是刪除用戶,好比說這個過程只需用你的身份登錄而且 POST 數據到 【a.com/delUser 】, 就能夠實現刪除操做。好如今說B網站,B網站被攻擊了,別人種下了惡意代碼,你點開的時候就會模擬跨域請求,若是是針對你,那麼就能夠模擬對 A 站的跨域請求,剛好這個時候你已經在 A 站登錄了。那麼攻擊者 在B 站內經過腳本,模擬一個用戶刪除操做是很簡單的。面對這種問題,有從瀏覽器解決,但我的認爲最好是從網站端解決,檢測每次 POST 過來數據時的 Refer,添加AccessToken 等都是好方法。web

防範 CSRF 攻擊能夠遵循如下幾種規則:ajax

  1. Get 請求不對數據進行修改
  2. 不讓第三方網站訪問到用戶 Cookie
  3. 阻止第三方網站請求接口
  4. 請求時附帶驗證信息,好比驗證碼或者 Token

什麼是 XSS 攻擊chrome

Cross-Site Scripting(跨站腳本攻擊)簡稱 XSS,是一種代碼注入攻擊。XSS 能夠分爲多種類型,可是整體上我認爲分爲兩類:持久型、非持久型和DOM-Based型 XSS. 持久型也就是攻擊的代碼被服務端寫入進數據庫中,這種攻擊危害性很大,由於若是網站訪問量很大的話,就會致使大量正常訪問頁面的用戶都受到攻擊。 一次性(非持久性) 經過用戶點擊連接引發

  • 反射型 XSS,也可稱爲非持久型 XSS。
  • 其攻擊方式每每是經過誘導用戶去點擊一些帶有惡意腳本參數的 URL 而發起的。
  • 事實上因爲反射型 XSS 由於 URL 特徵致使很容易被防護。不少瀏覽器如 Chrome 都內置了 相應的 XSS 過濾器,來防範用戶點擊了反射型 XSS 的惡意連接
  • 反射型 XSS 歸根到底就是因爲不可信的用戶輸入被服務器在沒有安全防範處理,而後就直接使用到響應頁面中,而後反射給用戶而致使代碼在瀏覽器執行的一種 XSS 漏洞。

XSS 主要作什麼事:

  • 竊取用戶 Cookie
  • 僞造請求
  • XSS 釣魚

防範措施: 針對 URL 編碼, HTML編碼, JS 編碼。 URL只能使用英文字母(a-zA-Z)、數字(0-9)、-_.~4個特殊字符以及全部(; , /?:@&=+$#)保留字符。 例如:

// 使用了漢字
var url1 = 'http://www.帥.com';

而後因爲 encodeURI 不轉義 & 、 ? 和 = 。
    使用encodeURLComponent

// "http://a.com?a=%3F%26"
encodeURI('http://a.com') + '?a=' + encodeURIComponent('?&');
相應的解碼
decodeURl()
decodeURLComponent()
複製代碼

判斷輸入格式: 過濾特殊字符:<、 > 、&、
過濾危險字符: 去除<"script"> 、javascript、onclik

/** * 轉義 HTML 特殊字符 * @param {String} str */
function htmlEncode(str) {
    return String(str)
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
}

// 正則獲取危險標籤
var REGEXP_TAG = /<(script|style|iframe)[^<>]*?>.*?<\/\1>/ig;
// 正則獲取危險標籤屬性
var REGEXP_ATTR_NAME = /(onerror|onclick)=([\"\']?)([^\"\'>]*?)\2/ig;

/** * 過濾函數 */
function filter(str) {
    return String(str)
        .replace(REGEXP_TAG, '')
        .replace(REGEXP_ATTR_NAME, '');
}
複製代碼

或者使用 CSP 創建白名單,開發者明確告訴瀏覽器哪些外部資源能夠加載和執行。 兩種方式開啓 CSP。

  1. 設置 HTTP Header 中的 Content-Security-Policy
  2. 設置 meta 標籤的方式 

Content-Security-Policy: default-src ‘self’

Content-Security-Policy: img-src https://*

Content-Security-Policy: child-src 'none'

怎麼實現跨域訪問?

  1. JSONP

JSONP跨域GET請求是一個經常使用的解決方案,下面咱們來看一下JSONP跨域是如何實現的,而且探討下JSONP跨域的原理。 利用在頁面中建立"script"節點的方法向不一樣域提交HTTP請求的方法稱爲JSONP,這項技術能夠解決跨域提交Ajax請求的問題。

JSONP的工做原理以下所述:

假設在http://example1.com/index.php這個頁面中向http://example2.com/getinfo.php提交GET請求,咱們能夠將下面的JavaScript代碼放在http://example1.com/index.php這個頁面中來實現: 代碼以下:

//server.js
const url = require('url');

require('http').createServer((req, res) => {
    // console.log('req: ', req);

    const data = {
        x: 10
    };
    console.log('url.parse(req.url, true).query: ', url.parse(req.url, true).query);
    const callback = url.parse(req.url, true).query.callback;
    res.writeHead(200);
    res.end( `${callback}(${JSON.stringify(data)})` ); // jsonpCallback(data)

}).listen(3000, '127.0.0.1');

console.log('啓動服務,監聽 127.0.0.1:3000');

//html
<
script >
    function jsonpCallback(data) {
        alert('得到 X 數據:' + data.x);
    } <
    /script> <
script src = "http://127.0.0.1:3000?callback=jsonpCallback" > < /script>
複製代碼

當GET請求從http://xx1.html返回時,能夠返回一段JavaScript代碼,這段代碼會自動執行,能夠用來負責調用http://xxx2.html頁面中的一個callback函數。

JSONP的優勢是:它不像XMLHttpRequest對象實現的Ajax請求那樣受到同源策略的限制;它的兼容性更好,在更加古老的瀏覽器中均可以運行,不須要XMLHttpRequest或ActiveX的支持;而且在請求完畢後能夠經過調用 callback 的方式回傳結果。

JSONP的缺點則是:它只支持GET請求而不支持POST等其它類型的HTTP請求;它只支持跨域HTTP請求這種狀況,不能解決不一樣域的兩個頁面之間如何進行 JavaScript 調用的問題。 再來一個例子:

//server.js
const url = require('url');

require('http').createServer((req, res) => {
    // console.log('req: ', req);

    const data = {
        "x": "10"
    };
    console.log('url.parse(req.url, true).query: ', url.parse(req.url, true).query);
    if (url.parse(req.url, true).query.word === 'sjh') {
        const callback = url.parse(req.url, true).query.callback;
        res.writeHead(200);
        res.end( `${callback}(${JSON.stringify(data)})` );
    }

}).listen(3000, '127.0.0.1');

console.log('啓動服務,監聽 127.0.0.1:3000');

//html
var qsData = {
    'word': 'shj'
};
$.ajax({
    async: false,
    url: "http://127.0.0.1:3000", //跨域的dns
    type: "GET",
    dataType: 'jsonp',
    jsonp: 'callback',
    data: qsData,
    timeout: 5000,
    success: function(json) {
        console.log('json: ', json);
        alert(json.x)
        // let obj = JSON.parse(json);
        // console.log('obj: ',obj );
    },
    error: function(xhr) {
        console.log('xhr: ', xhr);
        //請求出錯處理 
        alert("請求出錯)", xhr);
    }
});
複製代碼

這種方式實際上是上例.ajax({..}) api  的一種高級封裝,有些.ajax api底層的參數就被封裝而不可見了。 jquery .ajax方法名有誤導人之嫌。 若是設爲dataType: 'jsonp',這個.ajax方法就和ajax XmlHttpRequest沒什麼關係了,這種跨域方式其實與ajax XmlHttpRequest協議無關了。 取而代之的則是JSONP協議。JSONP是一個非官方的協議,它容許在服務器端集成Script tags返回至客戶端,經過javascript callback的形式實現跨域訪問。

Jsonp的執行過程以下: 首先在客戶端註冊一個callback (如:'jsoncallback'), 而後把callback的名字(如:jsonp1236827957501)傳給服務器。注意:服務端獲得callback的數值後,要用jsonp1236827957501(......)把將要輸出的json內容包括起來,此時,服務器生成 json 數據才能被客戶端正確接收。

而後以 javascript 語法的方式,生成一個function, function 名字就是傳遞上來的參數 'jsoncallback'的值 jsonp1236827957501 . 最後將 json 數據直接以入參的方式,放置到 function 中,這樣就生成了一段 js 語法的文檔,返回給客戶端。

客戶端瀏覽器,解析script標籤,並執行返回的 javascript 文檔,此時javascript文檔數據,做爲參數, 傳入到了客戶端預先定義好的 callback 函數(如上例中jquery $.ajax()方法封裝的的success: function (json))裏。 能夠說jsonp的方式原理上和是一致的(qq空間就是大量採用這種方式來實現跨域數據交換的)。JSONP是一種腳本注入(Script Injection)行爲,因此有必定的安全隱患。 那jquery爲何不支持post方式跨域呢?

  1. 跨域資源共享CORS

    什麼是CORS CORS(cross-Origin Resource Sharing) 跨域資源共享,管理跨源請求。而跨域資源共享是有益的。今天的咱們建立的大多數網站加載資源從網絡的各個不一樣的地方, 當咱們發送 GET 請求時,大多數狀況瀏覽器頭會fan hui Access-Control-Allow-Origin: * 。意思是可以共享資源在任何域名下。不然就是隻能在特定的狀況下了。 當請求時如下時,將在原始請求前先進行標準預請求,使用 OPTIONS 頭,

http: //www.example.com/foo-bar.html
複製代碼

瀏覽器訪問外部資源時會出現下面這種狀況

same-origin

而咱們想要的效果是這種狀況.

cros-origin

咱們能夠在 HTTP 請求頭加上 CORS 標準。

  • Access-Control-Allow-Origin

  • Access-Control-Allow-Credentials

  • Access-Control-Allow-Headers

  • Access-Control-Allow-Methods

  • Access-Control-Expose-Headers

  • Access-Control-Max-Age

  • Access-Control-Request-Headers

  • Access-Control-Request-Method

  • Origin

    當咱們發送 GET 請求時,大多數狀況瀏覽器頭會返回 Access-Control-Allow-Origin: * 。意思是可以共享資源在任何域名下。不然就是隻能在特定的狀況下了。

    app.use(cors()); 當請求時如下時,將在原始請求前先進行標準預請求,使用 OPTIONS 頭,

    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH

    當咱們發的預處理請求並指示原始請求是否安全,若是指定原始請求時安全的,則它將容許原始請求。不然拒接。

OPTIONS

//npm install cors

    response.setHeader('Content-Type', 'text/html');

    var express = require('express');
    var cors = require('cors');

    //或者直接設置
    // res.writeHead(200, {
    //'Access-Control-Allow-Origin': 'http://localhost:8080'
    // });
    var app = express();

    app.use(cors());

    app.get('/hello/:id', function(req, res, next) {
        res.json({
            msg: 'CORS-enabled!'
        });
    });

    app.listen(80, function() {
        console.log('CORS-enabled web ');
    });
複製代碼
  1. Server Proxy:

    服務器代理,當你須要有跨域請求額操做時發送給後端,讓後端幫你帶爲請求,而後將最後的獲取的結果發送給你。

//html
$.get('http://127.0.0.1:3000/topics', function(data) {
    consoel.log(data)
})

//server.js
const url = require('url');

const http = require('http');

const server = http.createServer((req, res) => {
    const path = url.parse(req.url).path.slice(1);
    console.log('path: ', path);

    if (path === 'topics') {
        http.get('http://cnodejs.org/api/v1/topics', (resp) => {
            const {
                statusCode
            } = resp;
            const contentType = resp.headers['content-type'];
            console.log('resp: ', statusCode, contentType);
            let error;
            if (statusCode !== 200) {
                error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}` );
            } else if (!/^application\/json/.test(contentType)) {
                error = new Error('Invalid content-type.\n' +
`Expected application/json but received ${contentType}` );
            }
            if (error) {
                console.error(error.message);
                // Consume response data to free up memory
                resp.resume();
                return;
            }

            res.setEncoding('utf8');
            let data = "";
            resp.on('data', chunk => {
                data += chunk;
            });
            res.on('end', () => {
                try {
                    const parsedData = JSON.parse(rawData);
                    console.log(parsedData);
                } catch (e) {
                    console.error(e.message);
                }
            });
        })
    }
}).listen(3000, '127.0.0.1');

console.log('啓動服務,監聽 127.0.0.1:3000');
複製代碼
  1. document.domain + iframe

基於iframe實現的跨域要求兩個域具備aa.xx.com, bb.xx.com這種特色,也就是兩個頁面必須屬於一個基礎域(例如都是xxx.com,或是xxx.com.cn),使用同一協議(例如都是 http)和同一端口(例如都是80),這樣在兩個頁面中同時添加document.domain,就能夠實現父頁面調用子頁面的函數,固然這種方法只能解決主域相同而二級域名不一樣的狀況。 代碼以下:

//頁面一在head內添加js以下:
document.domain = 「xx.com」;

function aa() {
    alert(「p」);
}
//body添加iframe和js以下
<
iframe src = 」http: //localhost:8080/2.html「 id=」i」>

    document.getElementById(‘i’).onload = function() {
        var d = document.getElementById(‘i’).contentWindow;
        d.a();
    };

//頁面二 head添加以下
document.domain = 「xx.com」;

function a() {
    alert(「c」);
}
//這時候父頁面就能夠調用子頁面的a函數,實現js跨域訪問
複製代碼

6.一、經過location.hash跨域

如下三種都是經過 iframe 方式來,咱們根據 iframe 可以在瀏覽器下可以跨域的特色,進行通訊。

在 url 中,www.a.com#la 的 "#la" 就是 location.hash,改變 hash 值不會致使頁面刷新,因此能夠利用 hash 值來進行數據的傳遞,固然數據量是有限的。

//locationNameA
<
script >

    function startRequest() {
        var ifr = document.createElement('iframe');
        ifr.style.display = 'none';
        ifr.src = 'http://127.0.0.1:5501/locationNameB.html#name';
        document.body.appendChild(ifr);
    }

function checkHash() {
    try {
        var data = location.hash ? location.hash.substring(1) : '';
        if (console.log) {
            console.log('Now the data is ' + data);
        }
    } catch (e) {};
}

setInterval(checkHash, 2000);

<
/script>
//locationNameB
<
script >

    function callBack() {
        try {
            parent.location.hash = 'somedata';
        } catch (e) {
            // ie、chrome的安全機制沒法修改parent.location.hash,
            // 因此要利用一箇中間的cnblogs域下的代理iframe
            var ifrproxy = document.createElement('iframe');
            ifrproxy.style.display = 'none';
            ifrproxy.src = 'http://127.0.0.1:5502/locationNameC.html#sjh';
            document.body.appendChild(ifrproxy);
        }
    }

    <
    /script>

    //locationNameC
    <
    script >
    parent.parent.location.hash = self.location.hash.substring(1);
console.log('parent.parent.location.hash: ', parent); <
/script>
複製代碼

6.2 經過window.name跨域

window對象有個 name 屬性,該屬性有個特徵:即在一個窗口(window)的生命週期內, 窗口載入的全部的頁面都是共享一個window.name 的,每一個頁面對 window.name 都有讀寫的權限,window.name 是持久存在一個窗口載入過的全部頁面中的。window.name 屬性的 name 值在不一樣的頁面(甚至不一樣域名)加載後依舊存在(若是沒修改則值不會變化),而且能夠支持很是長的 name 值(2MB)。

window.name = data; //父窗口先打開一個子窗口,載入一個不一樣源的網頁,該網頁將信息寫入。 
 window.location = 'http://a.com/index.html';//接着,子窗口跳回一個與主窗口同域的網址。
 var data = document.getElementById('myFrame').contentWindow.name。//而後,主窗口就能夠讀取子窗口的window.name了。

複製代碼

若是是與iframe通訊的場景就須要把iframe的src設置成當前域的一個頁面地址。

//http://127.0.0.1:5502/window.nameA.html
let data = '';
const ifr = document.createElement('iframe');
ifr.src = "http://127.0.0.1:5501/window.nameB.html";
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function() {
        ifr.onload = function() {
            console.log('ifr。contentWindow: ', ifr.contentWindow, ifr.contentWindow.name);
            data = ifr.contentWindow.name;
            console.log('收到數據:', data);
        }
        ifr.src = "http://127.0.0.1:5502/window.nameC.html";
    } <
    /script>  

    //http://127.0.0.1:5501/window.nameB.html
    <
    script >
    window.name = "你想要的數據!"; <
/script>

//http://127.0.0.1:5502/window.nameC.html
emty
複製代碼

6.3 windows.postMessage

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

接收數據:data、type:類型、source:對象、origin:源;

//postMessageChild.html
<
iframe src = "./postMessageChild.html"
id = "myFrame" > < /iframe> <
script >

    iframe = document.getElementById('myFrame')

iframe.onload = function() {

    iframe.contentWindow.postMessage('MessageFromIndex1', '*')

    setTimeout(() => {
        iframe.contentWindow.postMessage('MessageFromIndex2', '*')
    })

}

function receiveMessageFromIframePage(event) {
    console.log('receiveMessageFromIframePage', event)
}

window.addEventListener('messae', receiveMessageFromIframePage, false)

// postMessageParent.html
parent.postMessage({
    msg: 'MessageFromIframePage'
}, "*");

function receiveMessageFromParent(event) {
    consle.log('receiveMessageFromParent', event)
}

widnow.addEventListener('message', receiveMessageFromParent, false)
複製代碼
  1. websocket

WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)做爲協議前綴。該協議不實行同源政策,只要服務器支持,就能夠經過它進行跨源通訊。

//html
<
ul > < /ul> <
input type = "text" >
    $(function() {
        var iosocket = io.connect('http://localhost:3000/');
        var $ul = $("ul");
        var $input = $("input");
        iosocket.on('connect', function() { //接通處理
            $ul.append($('<li>連上啦</li>'));
            iosocket.on('message', function(message) { //收到信息處理
                $ul.append($('<li></li>').text(message));
            });
            iosocket.on('disconnect', function() { //斷開處理
                $ul.append('<li>Disconnected</li>');
            });
        });

        $input.keypress(function(event) {
            if (event.which == 13) { //回車
                event.preventDefault();
                console.log("send : " + $input.val());
                iosocket.send($input.val());
                $input.val('');
            }
        });
    });

//server.js
var io = require('socket.io')(3000);
io.sockets.on('connection', function(client) {
    client.on('message', function(msg) { //監聽到信息處理
        console.log('Message Received: ', msg);
        client.send('服務器收到了信息:' + msg);
    });
    client.on("disconnect", function() { //斷開處理
        console.log("client has disconnected");
    });
});
console.log("listen 3000...");
複製代碼

後記

歡迎關注微信公衆號!,進行更好的交流。
複製代碼

微信
相關文章
相關標籤/搜索