什麼是跨域?爲什麼要禁止跨域?怎樣跨域?

 

什麼是跨域

現代瀏覽器出於安全考慮,都會去遵守一個叫做“同源策略”的約定,同源的意思是兩個地址的協議、域名、端口號都相同的情況下,才叫同源。這個時候兩個地址纔可以相互訪問 cookie、localStorage、sessionStorage、發送 ajax 請求,如果三者有一個不同,就是不同源,這時再去訪問這些資源就叫做跨域

爲什麼禁止跨域

跨域訪問會造成很多安全問題,下面我們來看一下常見的兩種跨域請求:

  • ajax 請求:A.com 中 請求 B.com 的接口,這個時候請求會帶上 B.com 的 cookie,通常會有登錄信息之類的,我們稱之爲 CSRF攻擊
  • dom 操作:A.com 中使用 iframe 嵌套了 B.com 的廣告,這時候如果 B.com 對 A.com 中的 dom 任意操作,那麼主網站 A.com 就崩了
  • 字體文件:字體文件也有跨域限制,只是我還不明白爲什麼

那麼哪些請求時不會造成跨域的呢

  • js、css、image 等靜態文件
  • form 表單提交

因爲這些請求不會攜帶 cookie,也就沒有跨域限制

怎麼跨域

瀏覽器對某些請求限制了跨域訪問,但是實際開發中,我們又確實需要去跨域訪問,怎麼辦呢?

jsonp

由於訪問 js 文件是沒有跨域限制的,我們可以利用這一點來做跨域。具體過程如下:

客戶端預先定義好一個 function 用來接收數據,然後動態的創建一個 script 插入到頁面中去後端請求數據

function run (data) {
    console.log(data.name)
}

var script = document.createElement('script')
script.src = 'http://xxx.js?callback=run'
document.body.appendChild(script)

服務端通過 callback 指定的函數名,將數據傳入其中,這樣 js 到達客戶端就能直接執行函數,把數據傳入預先定義好的函數中

run({ name: '張三' })

優點:

  • 無跨域限制
  • 兼容性好,古老的瀏覽器也可以使用,無需XMLHttpRequest或ActiveX支持

缺點:

  • 只能發起 get,不能發起 post
  • 只支持 http 請求,不能解決兩個不同源頁面之間的通信問題
  • 不像 ajax 可以返回詳細的狀態碼,用於錯誤處理
  • 頁面上會頻繁的插入 script 元素,如不及時清除,頁面將變得卡頓

window.name

該屬性有以下特徵

  • 每個窗口都會有自己獨立的 window 及 window.name
  • 在一個窗口的生命週期中(被關閉前),窗口中載入的所有頁面都共享一個 window.name,且每個頁面對 window.name 都有讀寫的權限
  • window.name 一直存在當前窗口,即使有新的頁面進來也不會改變它的值
  • window.name 可以存儲不超過 2M 的數據,數據格式可以自定義

document.domain + iframe 跨子域

默認情況下 document.domain 存放的是載入文檔的主機名,例如 a.main.com。可以手動修改這個值,但是有限制:

  • 只能改成當前域名 a.main.com 或上級域名 main.com。
  • 必須包含一個 ".",也就是說不能設置成頂級域名 com

利用這一點,對於主域名相同,而子域名不同的兩個頁面,例如 a.main.com 使用 iframe 嵌入了 b.main.com,可以使用 document.domain 來跨域。

當在 a 頁面直接通過 iframe.contentWindow 想去操作 b 頁面,或者 b 頁面通過 window.top、window.parent 想去操作 a 頁面時,就是出現跨域限制。

這個時候我們可以在 a、b 兩個頁面同時設置 document.domain = 'main.com',然後再去相互訪問就沒有問題了

location.hash + iframe

我們都知道,頁面地址中 hash 值的改變是不會引起頁面刷新的,所以我們可以利用 hash 值來在不同域中傳遞數據。例如

a.com 使用 iframe 嵌入了 b.com。a 頁面要向 b 頁面傳遞數據就可以通過改變 b 頁面的 hash 值,具體過程如下:

a 頁面:iframe.src = b.com#age=18

b 頁面:window.onhashchange = function () {console.log(localtion.hash)}

缺點:

  • 只能從父窗口向子窗口單項傳遞數據
  • 由於數據是放在url上的,所以有長度限制

postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,它可以解決以下幾個需求場景:

  • 多窗口之間的消息傳遞
  • 主頁面與嵌入在 iframe 內的頁面之間的消息傳遞

舉例說明一下用法,比如 a 頁面內通過 iframe 嵌入了 b 頁面,現在要互相傳遞數據

a -> b

  • a 頁面:iframe.contentWindow.postMessage('xxxx', 'http://b.com')
  • b 頁面:window.addEventListener('message', function(e) { console.log(e.data) })

b -> a

  • a 頁面:window.addEventListener('message', function(e) { console.log(e.data) })
  • b 頁面:window.top.postMessage('xxxxx', 'http://a.com')

這裏需要注意幾點

  • postMessage 的第一個參數是消息體,格式無限制,可以是 String、Array、Object
  • 第二個參數是目標窗口的地址,遵守同源策略,如果不同源,則目標窗口接收不到消息。如果傳入的時 “*”,則任意窗口都可以獲取到

服務器代理

瀏覽器有跨域限制,但是服務器不存在跨域問題。所以可以瀏覽器可以把需要跨域的請求先發給服務器,由服務器去請求資源,然後再把結果返回給瀏覽器。

典型的應用場景:前後端分離

前後端分別有一個域名,前端訪問服務端接口就出現了跨域限制,這個時候前端就可以在所有的服務端接口前加上一個標誌如 /api,然後在前端服務器和後端服務器前在加一層分發,帶 /api接口的都訪問後端服務器,並去掉 /api,其他的都訪問前端服務器。

WebSocket協議跨域

WebSocket  protocol 是 HTML5 中的一種新協議,它實現了瀏覽器與服務端全雙工通信,同時支持跨域。拿比較知名的websocket 庫 Socket.io 來舉例說明它的用法:

瀏覽器

var socket = io('http://localhost:8080')
socket.on('connect', function () {
    socket.on('message', function(msg) {
        console.log('收到消息:' + msg)
    })
    socket.on('disconnect', function() {
        console.log('關閉連接')
    })
})

服務器

socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 斷開處理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

跨域資源訪問(CORS)

CORS 時 W3C 的標準,它允許瀏覽器向跨源服務器發起 ajax 請求,從而達到跨域訪問的目的。這是當前比較流行的跨域方式,知識點也比較多,下面我們一一展開。

瀏覽器將 CORS 請求分爲兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)

簡單請求需要同時滿足以下兩個條件:

1、請求方法是一下三種之一

  • HEAD
  • GET
  • POST

2、請求頭不能超出一下幾個字段

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Context-Type 只限於三個值:application/x-www-form-urlencoded、multipart-part/form-data、text/plain

除此以外的就是非簡單請求

簡單請求

當瀏覽器判斷此次是一個跨域的 ajax 簡單請求時,會自動在 request header 中加入 Origin 字段,用來標識本次請求來自哪個源(協議、域名、端口號),服務器根據這個值決定是否允許本次請求。

如果服務器同意本次請求,會在 response header 中加入以下幾個字段

  • Allow-Control-Allow-Origin:它的值爲請求時的 Origin 字段或者 *,表示允許跨域的來源,* 表示允許所有的源來跨域訪問
  • Allow-Control-Allow-Credentials:布爾值,表示服務器是否接受來自瀏覽器跨域請求時所攜帶的 cookie
  • Allow-Control-Expose-Headers:瀏覽器通過 XMLHttpRequest 對象的 getResponseHeader 方法只能拿到 6 個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Prama。如果瀏覽器想拿到其他字段,就需要服務器在改字段中指定

默認情況下,CORS 請求是不攜帶 cookie 的。如果需要在 request header 中放入 cookie,除了上面說的服務器需要在 response header 中設置 Allow-Control-Allow-Credentials: true,表示願意接受跨域 cookie 之外,瀏覽器也需要設置一個參數表示允許發送 cookie

xhr.withCredentials = true;

非簡單請求

我們先來看一個 http 請求

var xhr = new XMLHttpRequest()
xhr.open('DELETE', 'http://xxx/deluser', true)
xhr.setRequestHeader('token', '888')
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.send()

預檢請求(prelight)

根據上面我們對簡單請求的約定,發現這條請求的方法、頭字段、Content-Type 的值都不符合簡單請求的約定。所以瀏覽器會判斷出此次的 http 請求是一次非簡單請求。

瀏覽器會先發出一條 http 請求詢問服務器,當前源是否在服務器的允許名單之內,以及可以使用哪些頭信息。我們稱這次詢問爲“預檢”(prelight),預檢的請求方法是 options,request header 中會有這麼幾個重要的字段:

  • Origin:表示請求來自哪個源
  • Access-Control-Request-Method:表示瀏覽器的 CORS 請求將會用到哪些 http 方法
  • Access-Control-Request-Headers:該字段是以都好分割的字符串,表示 CORS 請求會額外攜帶的頭字段

服務器接收到“預檢”請求後,會檢查上述三個字段,如果允許跨域就做出相應頭信息

  • Access-Control-Allow-Origin: http://api.bob.com
  • Access-Control-Allow-Methods: GET, POST, DELETE
  • Access-Control-Allow-Headers: token
  • Access-Control-Allow-Credentials: true
  • Content-Type: application/json; charset=utf-8

正常請求

瀏覽器在接收到服務器的“預檢”相應後,判斷出服務器允許跨域。這時便會發送正常的請求,只是在每次請求中,都會往 request header 中加入 Origin 字段,相應的服務器也會在每次相應中,在 response header 加入 Access-Control-Allow-Origin 字段

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