1. 程式人生 > 實用技巧 >關於跨域及其9種解決方案

關於跨域及其9種解決方案

跨域

什麼是跨域?
違反了瀏覽器同源策略的都是跨域

同源策略

何謂之同源?
同源即同協議、同域名、同埠,否則為跨域
同源策略會阻止一個域的JavaScript指令碼和另外一個域的內容進行互動

跨域的表現

跨域例:
http://www.test.cn:3000
https://www.test.cn:3000

不同源時,以下操作會受影響的:

  • js操作本地儲存如cookie、LocalStorage
  • js操作頁面DOM元素
  • 可以傳送ajax請求並且被服務端正常響應,但響應結果會被瀏覽器攔截(Cross-Origin Read Blocking)

為什麼瀏覽器不支援跨域

  • 安全方面,防止竊取cookie。使用者可能給惡意網站發請求,惡意網站拿到使用者cookie
  • DOM方面,如果可以操作DOM,可能嵌入iframe,造成安全問題

跨域的9種解決方案

  • jsonp
  • cors (cross origin resource sharing) 跨域資源共享
  • postMessage
  • document.domain(主域和副域名)
  • window.name
  • location.hash
  • nginx
  • websokcet(頁面之間通訊)
  • http-proxy

1 jsonp

jsonp, 即JSON padding,跨域獲取json的方式
瀏覽器攔截跨域請求,只針對js
因此,一些具有引入外部資源屬性的標籤,在引入時並不會被瀏覽器的同源機制攔截。在此基礎上可以有如下方案:

  • 通過link標籤的href
  • 通過img標籤的src
  • 通過script標籤的src

下面給出基於script的實現方案

  1. 建立script標籤,src屬性指向外部資源
  2. 在src上指定回撥函式,並將json資料攜帶在回撥函式的引數上響應給客戶端
  3. 客戶端執行回撥函式,獲取資料

方式一: 本地開啟服務,jsonp獲取資料

html:

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    <script>
        
        //建立script標籤,並引用外部資源
        function jsonp(params) {
            return new Promise(function (resolve, reject) {
                const { url, query, cb } = params
                const address = `${url}?wd=${query.wd}&cb=${cb}`
                let script = document.createElement('script')
                script.src = address
                // 執行回撥
                window[cb] = function (data) {
                    resolve(data)
                    // 垃圾回收
                    document.body.removeChild(script)
                }

                document.body.appendChild(script)
            })
        }

        const getJsonpRes = async function () {
            let rs = await jsonp({
                url: 'http://localhost:3000/say',
                // 查詢欄位
                query: { wd: 'island' },
                // 回撥函式
                cb: 'getCRData'
            })
            console.log('result from server:',rs)
            return rs
        }

        getJsonpRes();

    </script>

</body>

</html>

server:

let express = require('express')
let app = express()
app.get('/say', function(req, res){

    let {wd,cb} = req.query
    console.log(wd,cb)
    //將資料作為回撥函式的引數返回
    const jsonData = JSON.stringify([{name:"island"}]) 
    res.end( `${cb}(${jsonData})` )

})
app.listen(3000, () =>console.log('server run at 3000'))

方式二:使用百度搜索的服務

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    <script>
        //建立script標籤,並引用外部資源
        function jsonp(params) {
            return new Promise(function (resolve, reject) {
                const { url, query, cb } = params
                const address = `${url}?wd=${query.wd}&cb=${cb}`
                let script = document.createElement('script')
                script.src = address

                // 執行回撥
                window[cb] = function (data) {
                    //返回資料
                    resolve(data)
                    // 垃圾回收
                    document.body.removeChild(script)
                }

                document.body.appendChild(script)
            })
        }

        const getJsonpRes = async function () {
            let rs = await jsonp({
                url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
                // 查詢欄位
                query: { wd: 'island' },
                // 回撥函式
                cb: 'getCRData'
            })
            console.log(rs)
            return rs
        }

        const getJsonpRes2 = function () {
            jsonp({
                url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
                query: { wd: 'island' },
                cb: 'getCRData'
            }).then(rs => {
                console.log(rs)
            })
        }

        // getJsonpRes();
        // getJsonpRes2();

    </script>

</body>

</html>

缺陷:

  • 只支援get請求,不支援post、put、delete。
  • 不安全,可能有xss攻擊(可能返回一個script標籤)

2 cors

cors,即cross origin resource sharing,跨域資源共享
這種方式主要是在後端實現,通過設定各種響應頭,來保證響應資料不被瀏覽器攔截

客戶端

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    hello world 3000

    <script>
        let xhr = new XMLHttpRequest()
        // 第三個引數指定非同步或者同步
        // xhr.open('GET', 'http://localhost:4000/getData',true)

        // (4) 強制帶上憑證
        document.cookie = 'name=island'
        xhr.withCredentials = true
        
        //(2) 可選設定請求頭名字,如果設定了,服務端也要設定響應的Access-Control-Allow-Headers
        // xhr.setRequestHeader('name','island')

        // (3)
        xhr.open('PUT', 'http://localhost:4000/getData', true)
        
        xhr.onreadystatechange = function () {
            //304 快取
            if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
                console.log(xhr.response)
                // (5) 獲取返回頭
                console.log(xhr.getResponseHeader('name'))
            }
        }
        xhr.send()

    </script>

</body>

</html>

本地服務

const express = require('express')

const app = express()

//啟用靜態資源
app.use(express.static(__dirname))

app.listen(3000, ()=>console.log('server run at 3000'))

外部服務

const express = require('express')
const app = express()
//設定訪問白名單
let accessList = ['http://localhost:3000']
app.use(function (req, res, next) {

    let origin = req.headers.origin
    if (accessList.includes(origin)) {
        //設定允許跨域訪問服務的白名單,當設定為*的時候,設定攜帶cookie失效
        res.setHeader('Access-Control-Allow-Origin', origin)
        // 設定允許的請求頭名字
        res.setHeader('Access-Control-Allow-Headers', 'name')
        //get post是預設支援的,不用配,其他的需要配置
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        // 預檢options的存活時間,即都少秒之後重新預檢
        // res.setHeader('Access-Control-Max-Age',6)
        //允許攜帶cookie
        res.setHeader('Access-Control-Allow-Credentials', true)
        //允許設定響應頭
        res.setHeader('Access-Control-Expose-Headers','name')
        if (req.method === 'OPTIONS') {
            //OPTIONS請求不做任何處理,因為是試探預檢請求,確定介面正常才可以傳送請求體
            // res.end()
            console.log('接收到了OPTIONS請求')
        }
    }
    next()

})
app.get('/getData', function (req, res) {

    console.log('接收到了請求')
    res.end('data from 4000')

})
app.put('/getData', function (req, res) {

    console.log('接收到PUT了請求')
    // 設定響應頭
    res.setHeader('name', 'island header name')
    res.end('data from 4000')

})
app.listen(4000, () => console.log('server run at 4000'))

3 postMessage

主要api:
iframe.contentWindow
window.postMessage
onmessage, e.soure

實現:

  1. 通過iframe巢狀需要傳送資訊的網頁,並監聽onload事件保證網頁載入完畢
  2. 在load回撥裡通過iframe.contentWindow獲取對方網頁,使用iframe.contentWindow.
  3. postMessage向對方網頁傳送資訊,對方網頁通過window監聽onmessage獲取資訊
  4. 對方網頁在onmessage回撥中,e.source.postMessage向我放網頁傳送資訊
  5. 我方window監聽onmessage,獲取響應資訊。實現了雙向通訊

本地網頁

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    <!-- 借用iframe+onload事件+postMessage -->
    <iframe src="http://localhost:4000/b.html" id="frame" onload="load()" frameborder="0"></iframe>
    <script>
        function load(params){
            //和b視窗通訊
            let frame = document.getElementById('frame')
            // 給b視窗傳送資訊,postMessage(data,url)
            frame.contentWindow.postMessage('a send msg to b','http://localhost:4000')

            //監聽b的回覆
            window.onmessage = (e)=>{
                console.log(e.data)
            }
        }

    </script>

</body>
</html>

本地服務

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))

對方網頁

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    this is b.html
    <script>
        window.onmessage = (e)=>{
            console.log(e.data, e)
            //回覆給a視窗
            e.source.postMessage('b send msg to a', e.origin)
        }

    </script>

</body>
</html>

對方服務

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

4 window.name

思路:
iframe+onload

雖然iframe可以通過src引入外部網頁,但是不能拿到外部網頁的DOM或者屬性,因為這是跨域的
因此可以在第一次onload時,改變src,切換到同源網頁,此時name屬性依然還在,完成跨域獲取資料

iframe引用外部網頁,在外部網頁中設定window.name屬性
iframe監聽onload事件,第一次觸發onload事件時,改變iframe的src指向同源的window,並toggle bool
iframe第二次觸發onload事件,拿到iframe.contentWindow.name

本地頁面

<! DOCTYPE html>
<html lang="en">

<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>

<body>

    a和b是同域的 3000
    c是獨立的 4000
    a獲取c的資料

    a先引用c c把值放到window.name,把a頁面的iframe的引用地址改到b,name不會消失(標籤沒變,只是標籤的屬性變化,所以src變化window.name依然還在)

    <iframe id="frame" src="http://localhost:4000/c.html" onload="load()" frameborder="0"></iframe>
    <script>
        let first = true
        function load() {
            let frame = document.getElementById('frame')
            //雖然可以通過src引入外部網頁,但是不能拿到外部網頁的DOM或者屬性,因為這是跨域的
            // console.log(frame.contentWindow.name)
            
            // 初次進入,載入b頁面
            if (first) {
                frame.src = 'http://localhost:3000/b.html'
                first = false
                return
            } else {
                console.log(frame.contentWindow.name)
            }
        }

    </script>

</body>

</html>

本地頁面b,作為中間橋樑

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>

    this is b

</body>
</html>

本地服務,提供靜態資源訪問

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))

外部網頁

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <script>
        window.name = '這是4000下, c中的window name'

    </script>

</body>
</html>

外部服務,提供靜態資源訪問

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

5 hash

思路:

  1. a通過iframe引用c,並在url後面設定hash值hasha,監聽hashchange事件
  2. c用loaction.hash收到hasha之後,通過iframe引用和a同源的b,並在iframe的src上設定hash值hashc
  3. b通過location.hash拿到hashc,並通過window.parent.parent拿到視窗a,給a設定hashc,設定完之後,a的hashchange事件觸發,拿到c傳過來的hash值

本地服務和外部服務

const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))

本地網頁

<! DOCTYPE html>
<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    page a
    <!-- 
        路徑後面的hash值可以用來通訊
        a和b同
        c獨立域
     -->
    <!-- 目的a想訪問c -->
    <!-- a給c傳一個hash值,c收到hash值後把hash值傳給b -->
    <!-- c給hash值傳給b, b將結果放到a的結果中 -->
    <iframe src="http://localhost:4000/c.html#req_island" frameborder="0"></iframe>
    <script>
        window.onhashchange = function(){
            console.log(location.hash)
        }
    </script>
</body>
</html>

和本地網頁同源的網頁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    this is b
    <script>
        window.parent.parent.location.hash = location.hash
    </script>
</body>
</html>

外部網頁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 
        c拿到之後建立iframe指向b
        b和a同域
     -->
    <script>
        console.log('a傳給c的hash是:',location.hash)
        let iframe = document.createElement('iframe')
        iframe.src = 'http://localhost:3000/b.html#res_island'
        document.body.appendChild(iframe)
    </script>
</body>
</html>

6 domain

domain主要用於一級域名和二級域名之間的跨域通訊
只要一級域名和二級域名之間設定document.domain的值相同
就可以通過iframe.contentWindow拿到對方的window的屬性實現通訊

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 
        一級域名二級域名
        思路:
            a通過http://a.island.cn:3000/b.html:3000/a.html
            b是通過http://b.island.cn:3000/b.html:3000/b.html
            通過iframe,再用document.domain宣告是一家的域名,就可以訪問了
            侷限是主域名和副域名之間可以用
     -->
    hello a 
    <iframe id="frame" src="http://b.island.cn:3000/b.html" frameborder="0" onload="load()"></iframe>
     <script>
         //2 domain,告訴是一家的
         document.domain = 'island.cn'

         function load(){
            let frame = document.getElementById('frame')
            // 拿到b視窗的window下屬性,跨域
            frame.contentWindow.propsb
         }
     </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    hello b
    <script>
        document.domain = 'island.cn'
        window[propsb] = 'this is propsb from b'
    </script>
</body>

</html>

7 websocket

不同於基於JavaScript的ajax,websocket沒有跨域限制
websocket協議是ws,內部基於tcp
socket和http的本質區別在於,socket是雙向的,http是單向的
作為h5的高階API,websocket的一個缺陷是相容性不太好,但是有一個常用的庫相容性很強,即socket.io

建立websocket通訊

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   
     <script>
         //新建websocket通訊
         let socket = new WebSocket('ws://localhost:3000')
         //給websocket伺服器傳送資訊
         socket.onopen = function(){
             socket.send('island send')
         }
         //監聽socket伺服器返回的資訊
         socket.onmessage = function(e){
             console.log(e.data)
         }
     </script>
</body>
</html>

建立websocket服務

yarn add ws

let Websocket = require('ws')

let wss = new Websocket.Server({port:3000})

wss.on('connection',function(ws){
    ws.on('message',function(data){
        console.log(data)

        ws.send('island server response')
    })
})

8 Nginx 配置跨域

nginx跨域是最簡單的跨域
配置nginx.conf
//所有json字尾的,在nginx資料夾的json目錄下查詢
location ~..json{
# 指定根目錄下 json目錄
root json;
# 新增跨域頭,此來源將被列入白名單
add_header "Access-Control-Allow-Origin" "
"
}

9 webpack配置跨域

webpack可以在devServer中配置跨域,常見配置如下

target:表示目標資源的地址
pathRewrite:對該欄位重寫
changeOrigin:核心配置。為true時會將請求頭中的host欄位改成target
secure:設定https協議的代理,因為target預設不支援https協議,因此需要做額外的配置

關於https的跨域配置

參考掘金文章https://juejin.im/post/6844904166125469710