跨域解決方案


什麼是跨域

先來說一個概念就是同源,同源指的是協議,端口,域名全部相同

同源策略(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能可能都會受到影響。可以說Web是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。

同源策略是處於對用戶安全的考慮,如果非同源就會受到以下限制:

  • cookie不能讀取
  • dom無法獲得
  • ajax請求不能發送

但是事實是經常需要藉助非同源來提供數據,所以就需要進行跨域請求。

JSONP

JSONP是指JSON PaddingJSONP是一種非官方跨域數據交換協議,由於scriptsrc屬性可以跨域請求,所以JSONP利用的就是瀏覽器的這個“漏洞”,需要通信時,動態的插入一個script標籤。請求的地址一般帶有一個callback參數,假設需要請求的地址爲http://localhost:666?callback=show,服務端返回的代碼一般是show(數據)JSON數據,而show函數恰恰是前臺需要用這個數據的函數。JSONP非常的簡單易用,自動補全API利用的就是JSONP,下面來看一個例子:

// 前端請求代碼
function jsonp (callback) {
    var script = document.createElement("script"),
        url = `http://localhost:666?callback=${callback}`;
    script.setAttribute("src", url);
    document.querySelector("head").appendChild(script);
}
function show (data) {
    console.log(`學生姓名爲:${data.name},年齡爲:${data.age},性別爲${data.sex}`);
}
jsonp("show");
// 後端響應代碼
const student = {
    name: "zp1996",
    age: 20,
    sex: "male"
};
var callback = url.parse(req.url, true).query.callback;
res.writeHead(200, {
    "Content-Type": "application/json;charset=utf-8"
});
res.end(`${callback}(${JSON.stringify(student)})`);

JSONP雖說簡單易用,但是有一個很大問題,那就是JSONP只能進行get請求

CORS

CORS(跨域資源共享)是由W3C制定的跨站資源分享標準,可以讓AJAX實現跨域訪問。想要了解跨域的話,首先需要了解下簡單請求

  • 請求方式爲GET或者POST
  • 假若請求是POST的話,Content-Type必須爲下列之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 不含有自定義頭(類似於segmentfault自定義的頭X-Hit
對於簡單請求的跨域只需要進行一次http請求:


function ajaxPost (url, obj, header) {
    return new Promise((resolve, reject) => {
        var xhr = new XMLHttpRequest(),
        str = '',
        keys = Object.keys(obj);
      for (var i = 0, len = keys.length; i < len; i++) {
        str += `${keys[i]}=${obj[keys[i]]}&`;
      }
      str = str.substring(0, str.length - 1);
      xhr.open('post', url);
      xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
      if (header instanceof Object) {
        for (var k in header) 
            xhr.setRequestHeader(k, header[k]);
      }
      xhr.send(str);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
            resolve(xhr.responseText);
          } else {
            reject();
          }
        }
      }
    });
}
ajaxPost("http://localhost:666?page=cors", {
    name: "zp1996",
    age: 20,
    sex: "male"
})
.then((text) => { console.log(text); }, 
            () => { console.log("請求失敗"); });

// 後端處理
var postData = "";
// 註釋下,下面示例後臺代碼補充在此處
req.on("data", (data) => {
    postData += data;
});
req.on("end", () => {
    postData = querystring.parse(postData);
    res.writeHead(200, {
        "Access-Control-Allow-Origin": "*",
        "Content-Type": "application/json;charset=utf-8"
    });
    if (postData.name === student.name &&
        Number(postData.age) === student.age &&
        postData.sex === student.sex
         ) {
        res.end(`yeah!${postData.name} is a good boy~`);
    } else {
        res.end("No!a bad boy~");
    }
});

打開控制檯觀察可以發現,Network只是發出了一次請求,但是對於非簡單請求來說,需要兩次http請求,在真正的請求之前需要進行一次預請求,下圖是進行一次預請求的請求/響應:

clipboard.png

觀察響應頭,可以發現需要多出了兩個響應頭:
- Access-Control-Allow-Headers,用來指明在實際的請求中,可以使用那些自定義的http請求頭。
- Access-Control-Max-Age,用來指定此次預請求的結果的有效期,在有效期內則不會發出預請求,有點像緩存的感覺。

當然還有諸如好多這樣的響應頭,請大家自行搜索瞭解,這裏就不再過多介紹,下面來看下對於非簡單請求跨域的代碼處理:

// 前端請求代碼
ajaxPost("http://localhost:666?page=cors", {
    name: "zp1996",
    age: 20,
    sex: "male"
}, { "X-author": "zp1996" })
.then((text) => { console.log(text); }, 
            () => { console.log("請求失敗"); });

// 後端處理,補充在簡單請求代碼註釋處
if (req.method === "OPTIONS") {
    res.writeHead(200, {
        "Access-Control-Max-Age": 3000,
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "X-author",
        "Content-Type": "application/json;charset=utf-8"
    }); 
    res.end();
    return void 0;      
} 

CSS Text Transformation

既然可以利用script的“漏洞”來進行JSONP跨域,那麼是不是也可以利用css樣式寫可以進行跨域請求來進行跨域呢?答案肯定是yes,利用css還有一個好處那就是,當被注入攻擊腳本時,css儘管被注入,也不會引起什麼大的安全問題,頂多也就是把頁面的樣式給改變,而js被注入的話,cookie就有可能被盜取等一系列安全問題出現。大牛已經將其做的非常完善,大家可以去star王集鵠(zswang)CSST,這裏我就把我所理解給大家簡單的分享下:

// 前端代碼
const id = "csst",
    ele = document.querySelector(`#${id}`),
    head = document.querySelector("head");
function getStyle (ele, prop) {
    return getComputedStyle(ele, "").getPropertyValue(prop);
}
function loadCss (url) {
    return new Promise((resolve) => {
        const link = document.createElement("link");
         link.setAttribute("rel", "stylesheet");
        link.setAttribute("type", "text/css");
        link.setAttribute("href", url);
        ele.addEventListener("webkitAnimationStart", function () {
            resolve(getStyle(ele, "content"));
        });
        head.appendChild(link);
    });
}
loadCss(`http://localhost:666?page=data.css&id=${id}`).then((data) => {
    console.log(data);
});

// 後端代碼
function cssData (id) {
    return `
        @keyframes a{
            from{

            }
            to{
                color: red;
            }
        }
        #${id} {
            content: "這種是很好,但是隻能傳輸文本啊";
            animation: a 2s;
        }
    `;
}
res.writeHead(200, {
    "Content-Type": "text/css"
});
res.end(cssData(query.id));

通過代碼可以看出這種實現方式是靠元素的content來拿接收到的數據,所以傳輸的只能是文本。至於爲什麼要返回動畫?是因爲不利用動畫,無法來對css腳本加載進行監測,也就無法進行回調(由於谷歌/火狐不支持linkonloadonreadychange,所以利用animationstart事件)。

window.postMessage

window.postMessage 是一個安全的跨源通信的方法。一般情況下,當且僅當執行腳本的頁面使用相同的協議(通常都是 http)、相同的端口(http默認使用80端口)和相同的 host(兩個頁面的 document.domain 的值相同)時,才允許不同頁面上的腳本互相訪問。 window.postMessage 提供了一個可控的機制來安全地繞過這一限制,當其在正確使用的情況下。
window.postMessage解決的不是瀏覽器與服務器之間的交互,解決的是瀏覽器不同的窗口之間的通信問題,可以做的就是同步兩個網頁,當然這兩個網頁應該是屬於同一個基礎域名。
// 發送端代碼
var domain = "http://localhost",
    index = 1,
    target = window.open(`${domain}/postmessage-target.html`);
function send () {
    setInterval(() => {
        target.postMessage(`第${index++}次數據發送`, domain);
    }, 1000);
}
send();
// 接受端代碼
<div id="test">沒有數據過來啊<div>
<script type="text/javascript">
    var test = document.querySelector("#test");
    window.addEventListener("message", e => {
        if (e.origin !== "http://localhost") {
            return void 0;
        }
        test.innerText = e.data;
    });
</script>
上述代碼實現了向一個頁面向另一個發送數據,但是這麼寫往往有着一些“危險”,需要知道的是,postMessage是向document對象中,網絡連接有時會很慢,可能會出現些問題,所以最好的方式是接受頁面已經開始加載了,這時發送一個消息給發送端,發送端在開始向接收端發送數據。改進下:
// 發送端添加代碼
window.addEventListener("message", (e) => {
    if (e.data === "ok")
        send();
    else 
        console.log(e.data);
});

// 接受端的head裏面加上script標籤
<script type="text/javascript">
    opener.postMessage("ok", opener.domain);
</script>

window.name

window.name 的美妙之處:name 值在不同的頁面(甚至不同域名)加載後依舊存在,並且可以支持非常長的 name 值(2MB)

這個方式我基本上沒有用過,所以沒有過多的發言權,大家想了解這個技術的話,可以通過懌飛(圓心):使用 window.name 解決跨域問題,圓心大神解釋的非常透徹。

document.domain

將子域和主域的document.domain設爲同一個主域
前提條件:

  • 這兩個域名必須屬於同一個基礎域名
  • 而且所用的協議,端口都要一致,否則無法利用document.domain進行跨域

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章