淺談跨域問題的出現及其解決方案

項目源碼:https://github.com/trp1119/cross-domain

1 跨域問題的出現

1.1 什么是同源策略

同源策略(Same origin policy)是瀏覽器最核心也最基礎的安全功能,同源策略會阻止一個域的 javascript 腳本和另外一個域的內容進行交互。所謂同源(即指在同一個域)就是兩個頁面具有相同的協議(protocol)、主機(host)和端口號(port)。[1]

1.2 什么是跨域請求(非同源策略請求)

跨域請求,即非同源策略請求,指當前發起請求的域與該請求指向的資源所在的域不一致。[1]

當前頁面url 被請求頁面url 是否跨域 原因
http://www.test.com http://www.test.com/index.html 同源(協議、域名、端口號相同)
http://www.test.com/ https://www.test.com/index.html 跨域 協議不同(http/https)
http://www.test.com/ http://www.baidu.com/ 跨域 主域名不同(test/baidu)
http://www.test.com/ http://blog.test.com/ 跨域 子域名不同(www/blog)
http://www.test.com:8080/ http://www.test.com:7001/ 跨域 端口號不同(8080/7001)

1.3 跨域請求發生場景

  • 在現代前端開發中,我們經常需要調用第三方的服務接口(例如 mock server、fake api),隨著專業化分工的出現有很多專業的信息服務提供商為前端開發者提供各類接口,這種情況下就需要進行跨域請求。
  • 在前后端分離的項目中,前端后端分屬于不同的服務跨域問題在采用這種架構的時候就存在,而且現在很多項目都采用這種前后分離的方式。

1.4 同源策略帶來的跨域請求限制

  • 無法讀取非同源網頁的 Cookie、LocalStorageIndexedDB
  • 無法接觸非同源網頁的 DOM
  • 無法向非同源地址發送 AJAX 請求 [1]

舉例

數據服務器(server_database)配置(5000 端口)

/**
 * 數據服務器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('數據服務器啟動成功,運行在5000端口')
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源數據!'
  }
  res.send(data)
})

客戶端(靜態資源)服務器(server_static)配置(8000 端口)

/**
 * 客戶端(靜態資源)服務器
 */

let express = require('express'),
    app = express()

app.listen(8000, () => {
  console.log('客戶端(靜態資源)服務器啟動成功,運行在8000端口')
})

app.get('/queryInfo', (req, res) => {
  res.send({
    code: 0,
    msg: '同源數據!'
  })
})

app.use(express.static('./static'))

客戶端數據請求

<script>
  // 同源請求
  $.ajax({
    url: 'http://localhost:8000/queryInfo',
    method: 'get',
    dataType: 'json',
    success: (res) => {
      console.log(JSON.stringify(res))
    }
  })
  // 非同源請求(跨域請求)
  $.ajax({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
    dataType: 'json',
    success: (res) => {
      console.log(JSON.stringify(res))
    }
  })
</script>

客戶端數據請求結果

1.4-1.png

同源請求下,得到服務器返回數據

{"code":0,"msg":"同源數據!"}

非同源請求(跨域請求)下,瀏覽器報錯

Failed to load http://localhost:5000/queryInfo: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access.

tips:在未經允許的情況下,瀏覽器禁止 js 讀取另一個域名的內容。但瀏覽器并不阻止向另一個域名發送請求。

跨域請求(非同源策略請求)限制為瀏覽器限制,非服務器限制。無論是否是跨域請求,服務器均會返回數據。

1.4-2.png
1.4-3.png

2 為什么會有跨域限制

  • 保護cookie、LocalStorage 和 IndexedDB

    cookie 中存著 sessionID(登錄憑證)。當用戶訪問惡意網站時,如果沒有同源策略,那么這個網站就可以通過 js 訪問 document.cookie 得到用戶關于各個網站的 sessionID,如果這個 sessionID 在有效期內,惡意網站就可以利用 sessionID登錄各個網站,獲取用戶其他信息。 [2]

  • 保護DOM操作

    惡意網站通過 iframe 加載支付寶頁面,當用戶進入惡意網站后,誤以為是支付寶官方頁面,輸入用戶名、密碼等信息。如果沒有同源策略,惡意網站就可以通過 DOM操作 獲取到用戶輸入值,從而控制用戶賬戶。 [2]

  • 限制 ajax操作

    cookie 工作機制

    1. 客戶向 A 網站的服務器發送登錄請求,并攜帶賬號密碼數據

    2. A 網站的服務器校驗賬號密碼正確后,返回響應并給本地添加了cookie

    3. 之后客戶再次向 A 網站發起請求會自動帶上A網站存儲在本地的cookie

    4. A 網站的服務器從cookie中獲取賬號密碼數據后,返回登陸成功界面

    2-1.png

用戶登錄過支付寶后,支付寶在本地設置了 cookie 信息。如果沒有同源策略,當用戶訪問惡意網站時,惡意網站利用存儲在本地的 cookie 等信息通過 ajax 向支付寶發起登錄請求,從 ajax 回調中解析到用戶數據信息。[3]

3 八種跨域解決方案

3.1 JSONP

3.1.1 JSONP 可用前提

瀏覽器安全性和方便性是成反比的,十位數的密碼提高了安全性,但是不方便記憶。同樣,同源策略提升了 Web 前端的安全性,但犧牲了Web拓展上的靈活性。

設想若把 htmljs、css、flashimage 等文件全部布置在一臺服務器上,小型網站這樣還可以,大中型網站如果這樣做服務器無法承受。為解決服務器冗余,在實型前后端分離,靜態資源服務器、圖片資源服務器、視頻資源服務器等拆分后,雖然系統變的更加靈活,但受制于瀏覽器同源策略限制,不同服務器之間通信受到限制。雖然保證了安全,Web 方便性大打折扣。

所以,現代瀏覽器在安全性和可用性之間選擇了一個平衡點。在遵循同源策略的基礎上,選擇性地為同源策略“開放了后門”。 例如 script、 img、link、iframe 等標簽,都允許垮域引用資源,嚴格說這都是不符合同源要求的。(當然,用戶只能是引用這些資源而已,并不能讀取這些資源的內容。例如在自己域內可以讀取百度 logo 圖片,但無法讀取到該數據的二進制資源。)[4]

利用瀏覽器允許 script 標簽跨域引用資源的特性,形成了一種非正式傳輸協議,JSONP。

3.1.2 一般 JSONP 使用方法及其實現原理

JSONPJSON with Padding ),一種非官方跨域數據交互協議。在使用時,用戶傳遞一個 callback 參數給服務端,然后服務端返回數據時會將這個 callback 參數作為函數名來包裹住 JSON 數據,這樣客戶端就可以隨意定制自己的函數來自動處理返回數據。[5]

3.1.2.1 JSONP 的使用

JSONP 客戶端調用方法

<script>
  // JSONP回調函數
  function fn (res) {
    console.log(res) // res 為從服務器獲取的數據
  }
</script>
<!-- 一般 JSONP 使用方法 -->
<script src="http://localhost:5000/queryInfo?callback=fn"></script>

JSONP 服務器配置

/**
 * 數據資源服務器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('數據服務器啟動成功,運行在5000端口')
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源數據!'
  }
  // JSONP 跨域數據返回
  let fn = req.query.callback // 獲取客戶端傳遞的函數名,注意,此處 callback 要與前端協商設置
  res.send(`${fn}(${data})`) // 返回指定格式的內容,函數名(數據) 這種格式
})

返回數據

3.1.2.1-1.png
3.1.2.2 JSONP 實現跨域請求原理
3.1.2.2-1.png

注意:客戶端定義的必須是全局函數,因為瀏覽器中收到服務器返回的函數執行只有在全局函數下才能運行。

3.1.2.2-2.png
3.1.3 ajax 下使用 JSONP及其原理

JSONP 是非官方跨域數據交互協議,但 ajax 對其進行了封裝,可采用 ajax 發起 jsonp 跨域請求。(axiosjsonp 請求方式)

3.1.3.1 使用 ajax 進行 JSONP 跨域請求

JSONP 客戶端調用方法

<script>
  // 采用 ajax 發起 JSONP 跨域請求
  $.ajax({
    url: 'http://localhost:5000/queryInfo',
    method: 'GET',
    dataType: 'jsonp', // 當 data-type 設置為 jsonp 的時候,實現的是 jsonp 跨域請求
    jsonp: 'callback', // 自定義傳遞時的 callback 名稱,默認為 'callback'
    // jsonpCallback: 'fn', // 自定義函數名
    success: (res) => {
      console.log(JSON.stringify(res)) // 從服務器獲取的結果
    }
  })
</script>

接口調用

3.1.3.1-1.png
3.1.3.1-2.png

返回數據

3.1.3.1-3.png
3.1.3.2 ajax 實現 JSONP 跨域請求原理

ajax 對 JSONP 的封裝依然才用的是 JSONP 實現原理,即通過動態創建 script 標簽,然后拼裝數據后發起請求。具體實現可參考 3.1.4 封裝一個簡單的 JSONP 實現返回 Promise

3.1.4 封裝一個簡單的 JSONP 實現返回 Promise

JSONP 封裝

;(function anonymous(window) {
  /**
   * JSONP 方法
   * url 請求的接口地址
   * options 配置項
   *    jsonp: 'callback'(默認值)
   *    jsonpCallback: 隨機生成的全局函數/自定義全局函數名
   *    timeout: 3000(默認值)
   */
  let jsonp = function (url, options = {}) {
    // 返回 Promise
    return new Promise((resolve, reject) => {
      // 驗證參數合法性
      if (typeof url === 'undefined') {
        reject('url必須傳遞!')
        return
      }
      // 發送 jsonp 請求
      let SCRIPT = document.createElement('script'),
          CALL_BACK = options.jsonp || 'callback',
          FN_NAME = options.jsonpCallback || `JSONP${new Date().getTime()}`,

      SCRIPT.src = `${url}${url.indexOf('?') >= 0 ? '&' : '?'}${CALL_BACK}=${FN_NAME}&_${new Date().getTime()}`
      document.body.appendChild(SCRIPT)

      // 成功或失敗后執行的函數
      window[FN_NAME] = function (result) {
        document.body.removeChild(SCRIPT)
        window[FN_NAME] = null
        resolve(result)
      }
    })
  }
  if (typeof module !== 'undefined' && module.exports !== 'undefined') {
    module.exports = {
      jsonp
    }
  }
  window.jsonp = jsonp

})(typeof window === 'undefined' ? global : window)
// 判斷正在不同的環境下去,讓 window 代表不同的全局對象,瀏覽器環境下就是 window,node 環境下執行代碼就是 global

使用自行封裝的 JSONP 發起跨域請求

<script src="jsonp.js"></script>
<script>
  // 自己封裝一個簡單地 jsonp 跨域請求,返回 Promise
  jsonp('http://localhost:5000/queryInfo').then(res => {
    console.log(JSON.stringify(res))
  })
</script>

接口調用

3.1.4-1.png
3.1.4-2.png

返回數據

3.1.4-3.png
3.1.5 JSONP 不足
  • JSONP 由于采用 script 資源文件請求,而資源請求為 GET 請求,故僅在 GET 請求中使用 JSONP 跨域請求。
  • 使用 ajax 請求網站,而服務器返回的 JSONP callback 是惡意執行代碼,導致返回瀏覽器后會自動執行惡意代碼,威脅數據安全。(XSS 攻擊)

3.2 CORS 跨域資源共享

CORS (Cross-Origin Resource Sharing),跨域資源共享

CORS 需要瀏覽器和服務器同時支持,才可以實現跨域請求,目前幾乎所有瀏覽器都支持 CORS,IE 則不能低于 IE10。CORS 的整個過程都由瀏覽器自動完成,前端無需做任何設置,跟平時發送 ajax 請求并無差異。所以,實現 CORS 的關鍵在于服務器,只要服務器實現 CORS 接口,就可以實現跨域通信。[6]

3.2.1 CORS 跨域配置方式

服務器未配置時限制跨域

3.2.1-1.png

由圖中可以看出,瀏覽器不允許跨域原因已指出,No “Access-Control-Allow-Origin” header,故可在服務端進行相關頭部信息配置以實現跨域請求。6

客戶端發送請求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  // 客戶端為普通的 axios get 請求 
  axios({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
  }).then(res => {
    console.log(res)
  })
</script>

服務端配置

/**
 * 數據資源服務器
 */

let express = require('express'),
    app = express()

app.listen(5000, () => {
  console.log('數據服務器啟動成功,運行在5000端口')
})

// 基于CORS設置允許跨域請求
app.use((req, res, next) => {
  // 允許哪些源可以向這個服務器發送AJAX請求(通配符是 '*',表示允許所有的源,也可以單獨設置某個源,'http://localhost:8000',這樣只允許 http://localhost:8000 的請求)
  // 不使用通配符是為了保證接口和數據的安全,不能讓所有的源都能訪問。而且一旦設置了允許攜帶憑證過來,則設置 '*' 通配符會被報錯,此時只能設置具體的源!且只能設置一個允許訪問的源。
  res.header('Access-Control-Allow-Origin', '*')
  // 是否允許跨域的時候攜帶憑證(例如 cookie 憑證,true 為允許,false 為不允許,設置為 false,客戶端和服務器之間不會傳遞 cookie,這樣 session 存儲就失效了)(session 之所以有用是因為客戶端從 cookie 中取 sid ,即將sid通過 cookie 傳遞給服務器進行校驗)
  // 一般都設置為 true
  res.header('Access-Control-Allow-Credentials', true)
  // 允許的請求頭部(哪些頭部信息是合法的)
  res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-Width, Cookie')
  // 允許的請求方式(一定要有 OPTIONS)
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEAD, OPTIONS')
  // 設置 OPTIONS 請求目的:我們吧這個請求當做一個試探性請求,當客戶端需要向服務器發送請求的時候,首先發送一個 OPTIONS 請求,服務器接受到是 OPTIONS 請求,看一下是否允許跨域,允許返回成功。如果服務器不允許跨域,則客戶端會出現跨域請求不允許的錯誤。如果客戶端檢測到不允許跨域,則后續的請求都不再進行。 =》 客戶端 axios 框架就是這樣處理的,自己寫的沒有寫 OPTIONS 請求。
  req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next()
  // next() 為 express 中間件語法
  next()
})

app.get('/queryInfo', (req, res) => {
  let data = {
    code: 0,
    msg: '非同源數據!'
  }
  // CORS 跨域數據返回
  res.send(data)
})

接口調用

3.2.1-2.png

返回數據

3.2.1-3.png
3.2.2 CORS 跨域配置介紹
3.2.2.1 Access-Control-Allow-Origin
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Origin', 'http://localhost:8000')

允許哪些源可以向這個服務器發送數據請求(通配符是 '*',表示允許所有的源,也可以單獨設置某個源,如 'http://localhost:8000',這樣只允許 http://localhost:8000 的請求)。

不使用通配符 '*' 是為了保證接口和數據的安全,即不能讓所有的源都能訪問。而且一旦設置了允許攜帶憑證過來,則設置 '*' 通配符會被報錯,此時只能設置具體的源!且只能設置一個允許訪問的源。 7 8

3.2.2.2 Access-Control-Allow-Credentials
res.header('Access-Control-Allow-Credentials', true)

是否允許跨域的時候攜帶憑證(例如 cookie 憑證,設置為 true 為允許攜帶 cookie 憑證,false 為不允許。設置為 false 客戶端和服務器之間不會傳遞 cookie,這樣 session 存儲就失效了)(session 之所以有用是因為客戶端從 cookie 中取 sid ,即將 sid 通過 cookie 傳遞給服務器進行校驗)

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  // 客戶端為普通的 axios get 請求 
  axios({
    url: 'http://localhost:5000/queryInfo',
    method: 'get',
    withCredentials: true, // 服務端和客戶端 withCredentials 屬性都要設置為 true,無則客戶端與服務端無法基于請求頭進行 cookie 傳遞
  }).then(res => {
    console.log(res)
  })
</script>

攜帶憑證需要客戶端設置 withCredentials: true,此時,若 Access-Control-Allow-Origin 設置為通配符 '*',即 res.header('Access-Control-Allow-Origin', '*') ,瀏覽器會報錯(因為任何域都攜帶憑證請求會影響安全)。只允許設置域形式。

3.2.2.2-1.png

同樣,若 Access-Control-Allow-Origin 設置多個 域,即 res.header('Access-Control-Allow-Origin', 'http://localhost:8000, http://localhost:8001') ,瀏覽器會報錯。

3.2.2.2-2.png

即在客戶端攜帶憑證請求情況下只能設置允許一個域的請求。 7 8

未設置 withCredentialstrue 時的請求頭(無 cookie

3.2.2.2-3.png

設置 withCredentialstrue 時的請求頭(有 cookie

3.2.2.2-4.png
3.2.2.3 Access-Control-Allow-Headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-Width, Cookie')

設置允許的請求頭部信息,即哪些頭部信息是合法的。

3.2.2.4 Access-Control-Allow-Methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEAD, OPTIONS')

允許的請求方式(一定要有 OPTIONS)。

3.2.3 CORS 跨域不足

若攜帶憑證發起請求,CORS 只能指定一個允許源,不能使用通配符和指定多個源。

3.3 node 作為中間件代理

Nodejsrequest 模塊是服務端發起請求的工具包,可在本服務器向其他域的服務器發起請求。使用前需安裝 request 插件。 9

yarn add request

客戶端發送請求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('/queryInfo').then((res) => {
    console.log(res)
  })
</script>

客戶端(靜態資源)服務器端配置

/**
 * 客戶端(靜態資源)服務器
 */

let express = require('express'),
    app = express()
    request = require('request')

app.listen(9000, () => {
  console.log('靜態資源服務器啟動成功,運行在9000端口')
})

app.get('/queryInfo', (req, res) => {
  // 在服務器向其他域發起請求
  request('http://localhost:5000/queryInfo', (err, response, body) => {
    res.send(body)
  })
})

app.use(express.static('./static'))

接口調用

3.3-1.png
3.3-2.png

返回數據

3.3-3.png

3.4 http proxy 代理

proxy 只是一層代理,用于把指定 path 代理去數據服務器提供的地址,他的背后是由 node server 提供服務的。

同源策略限制是瀏覽器進行限制的,服務器間相互數據請求并不受瀏覽器同源策略限制。

設置代理后,當客戶端請求某跨域接口時,實際請求的是客戶端所在服務器某個接口, 當客戶端服務器收到客戶端請求時,會根據代理設置,通過服務器間通信請求數據服務器的數據(跨域數據),請求到數據后,再將數據通過客戶端服務器返回給客戶端。

這樣,客戶端請求的是自身服務器接口,而不是跨域接口,不會受同源策略限制,實現跨域。 10

3.4.1 proxy 跨域設置
3.4.1.1 webpack 中 proxy 設置

webpack dev-server 使用了非常強大的 http-proxy-middleware 包用于解決跨域請求。

webpack.config.js 文件中進行配置。 11

簡單配置

devServer: {
  proxy: {
     "/queryInfo": "http://localhost:5000"
  }
}

復雜配置

devServer: {
  proxy: {
    '/queryInfo': {
      target: 'http://localhost:5000',
      changeOrigin: true
    }
  }
}

客戶端發送請求

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('/queryInfo').then(res => {
    console.log(res)
  })
</script>

接口調用

3.4.1.1-1.png

返回數據

3.4.1.1-2.png
3.4.1.2 vue 中 proxy 設置

vue.config.js 中進行配置。 12

簡單配置

簡單配置,這樣配置后全局接口都會被代理到 http://localhost:5000

module.exports = {
  // 簡單配置
  devServer: {
    proxy: 'http://localhost:5000'
  }
}

復雜配置

如果要配置部分接口代理,或進行 https 支持等設置,可進行復雜配置。

module.exports = {
  // 復雜配置
  devServer: {
    proxy: {
      '/queryInfo': {
        target: 'http://localhost:5000',
        ws: true,
        changeOrigin: true
      },
    }
  }
}

客戶端發送請求

<script>
import axios from 'axios'

export default {
  name: 'app',
  mounted () {
    // axios.get('http://localhost:5000/queryInfo').then((res) => {
    //   window.console.log(res)
    // })
    axios.get('/queryInfo').then((res) => {
      window.console.log(res)
    })
  }
}
</script>

接口調用

3.4.1.2-1.png

返回數據

3.4.1.2-2.png
3.4.1.3 react 中 proxy 設置

簡單配置

可在 package.json 中進行簡單配置,這樣配置后全局接口都會被代理到 http://localhost:500013

"proxy": "http://localhost:5000"

復雜配置

如果要配置部分接口代理,或進行 https 支持等設置,可在 src 目錄下新建 setupProxy.js 文件進行復雜配置。

使用此配置需先安裝 http-proxy-middleware 插件。 13

yarn add http-proxy-middleware
// setupProxy.js 設置
const proxy = require('http-proxy-middleware')

module.exports = function(app) {
  app.use(
    '/queryInfo',
    proxy({
      target: 'http://localhost:5000',
      changeOrigin: true,
    })
  )
}

客戶端發送請求

import axios from 'axios'

// axios.get('http://localhost:5000/queryInfo').then((res) => {
//   console.log(res)
// })

axios.get('/queryInfo').then((res) => {
  console.log(res)
})

接口調用

3.4.1.3-1.png

返回數據

3.4.1.3-2.png
3.4.2 proxy 缺點

僅在本地開發環境中使用,由于線上使用的是打包后的靜態文件,故需要線上環境服務器配置以支持跨域。

3.5 nginx 反向代理

使用 nginx 啟動客戶端服務(http://localhost:7000)跨域請求服務端數據(http://localhost:5000/queryInfo),此時受瀏覽器同源策略限制,瀏覽器會報錯。 14

客戶端發送請求

<script>
  // 客戶端為普通的 axios get 請求 
  axios.get('http://localhost:5000/queryInfo').then(res => {
    console.log(res)
  })
</script>
3.5-1.png

客戶端服務器配置

可在 niginx 服務器文件夾 conf/nginx.conf 下進行代理配置。

server {
    listen       7000; // 端口號設置
    server_name  localhost;

    location / {
        root   html/test; // 靜態資源文件夾
        index  index.html index.htm;
    }

    location /queryInfo { // 跨域代理設置
        proxy_pass http://localhost:5000;
    }
}

客戶端發送請求

設置后,客戶端 axios 請求需改為 axios.get('/queryInfo') 以形成訪問同源接口樣式。

<script>
  // 客戶端為普通的 axios get 請求 
  axios.get('/queryInfo').then(res => {
    console.log(res)
  })
</script>

接口調用

3.5-2.png

返回數據

3.5-3.png

3.6 window.name

3.6.1 window.name 特性

頁面在瀏覽器端展示的時候,總能在控制臺拿到一個全局變量 window,該變量有一個 name 屬性,其有以下特征: 15

  • 每個瀏覽器窗口都有獨立的 window.name 與之對應。
  • 在一個瀏覽器窗口的生命周期中(被關閉前),窗口載入的所有頁面同時共享一個 window.name,每個頁面對 window.name 都有讀寫的權限。
  • window.name 一直存在與當前窗口,即使是有新的頁面載入也不會改變 window.name 的。
  • window.name 可以存儲不超過 2M 的數據,數據格式按需自定義。

舉例

在 C頁面(http://localhost:5000/window.name/C.html)請求同源服務器,獲取到數據并賦值給window.name。

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
<script>
  axios.get('http://localhost:5000/queryInfo').then((res) => {
    window.name = JSON.stringify(res)
  })
</script>
3.6.1-1.png

此時,在同一瀏覽器窗口,將鏈接改為非同源的 A頁面(http://localhost:8000/window.name/A.html),發現此時仍可以通過 window.name 拿到數據。

3.6.1-2.png

利用這一點,可以試圖在 A頁面 用 iframe 加載 C頁面,然后取到其 window.name 中的值。

<body>
  <iframe src="http://localhost:5000/window.name/C.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    iframeBox.onload = () => {
      console.log(iframeBox.contentWindow.name)
    }
  </script>
</body>

運行發現瀏覽器進行了跨域請求限制。

3.6.1-3.png
3.6.2 利用 window.name 進行跨域

因為 A頁面 與 C頁面 一直存在于同一瀏覽器窗口內,window.name 值一直存在,所以可以使用無任何內容的 中間頁面 proxy.html(與 A頁面 同源),在 C頁面 加載后,將 iframesrc 值更改為中間頁面 proxy.html ,此時,proxy.html 頁面的 window.name 值與 C頁面 是一致的。由于 A頁面 與 中間頁面 proxy.html 同源,故 A頁面 此時可以取到 window.name 值。 15

<body>
  <iframe src="http://localhost:5000/window.name/C.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    let count = 0
    iframeBox.onload = () => {
      if (count === 0) {
        // 由于替換 src 后 iframe 會重新執行 onload,而執行 onload 后又會替換 src,故添加計數器以防止死循環
        iframeBox.src = "http://localhost:8000/window.name/proxy.html"
        count++
        return
      }
      console.log(iframeBox.contentWindow.name)
    }
  </script>
</body>
3.6.2-1.png

3.7 document.domain

3.7.1 document.domain 跨域使用

document.domain 用來得到當前網頁的域名兩個文檔,只有在 document.domain 都被設定為同一個值,表明他們打算協作;或者都沒有設定 document.domain 屬性并且 url 的域是一致的,這兩種條件下,一個文檔才可以去訪問另一個文檔。

如果不是因為這個特殊的策略,每一個站點都會成為他的子域的 XSS 攻擊的對象(例如,http://a.test.com 可以被來自 http://b.test.com 站點的惡意文件攻擊)。

利用兩個文檔 document.domain 相同即可協作的特性,在 A頁面 使用 iframe 加載 B頁面,并將 A頁與B頁面設置相同的 document.domain ,進行跨域獲取數據。 16

A頁面

// A頁面鏈接 http://a.test.com:5000/document.domain/A.html
<body>
  <iframe src="http://b.test.com:5000/document.domain/B.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    document.domain = 'test.com'
    iframeBox.onload = () => {
      console.log(iframeBox.contentWindow.data)
    }
  </script>
</body>

B頁面

<body>
  <script>
    document.domain = 'test.com'
    window.data = {
      code: 0,
      msg: '非同源數據!'
    }
    // axios.get('http://b.test.com:5000/queryInfo').then((res) => {
    //   console.log(res)
    //   window.data = JSON.stringify(res)
    // })
  </script>
</body>

返回數據

3.7.1-1.png
3.7.2 document.domain 的缺點

在根域范圍內,瀏覽器允許把 domain 屬性的值設置為它的上一級域。例如,在 a.test.com 域內,可以把domain 設置為 test.com 。 16

所以 document.domain 只能處理父域相同,子域不同的情況。

3.8 postMessage

受瀏覽器跨域限制,非同源的頁面無法進行通信,window.postMessage() 方法提供了一種受控機制來規避此限制。window.postMessage() 方法可以安全地實現 Window 對象之間的跨域通信。例如,在一個頁面和它生成的彈出窗口之間,或者是頁面和嵌入其中的 iframe 之間。

一般來說,一個窗口可以獲得對另一個窗口的引用(例如,通過 targetWindow=window.opener ),然后使用 targetWindow.postMessage() 在其上派發 MessageEvent。接收窗口隨后可根據需要自行處理此事件。傳遞給 window.postMessage() 的參數通過事件對象暴露給接收窗口。

3.8.1 postMessage API 與 onmessage API
3.8.1.1 postMessage API

targetWindow.postMessage(message, targetOrigin, [transfer]) 有三個參數,transfer 可選。 17

  • mesaage 就是要發送到目標窗口的消息。
  • targetOrigin 就是指定目標窗口的來源,必須與消息發送目標相一致。如果接收方窗口的協議、主機地址或端口這三者的任意一項不匹配 targetOrigin 提供的值,那么消息就不會被發送。值可以是字符串 “*”url 。 “*” 表示任何目標窗口都可接收。
  • transfer 是可選項,數組內的對象是實現 Transferable 接口的對象。它和 message 一樣會被傳遞給目標頁面,這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
3.8.1.1 onmessage API
window.onmessage = function(e){ }

參數 emessage 實例,里面包含了 dataorigin、source 等屬性,data 是發送方發送的 message , origin 是發送方所屬的域,source 是發送方的 window 對象的引用。

3.8.2 使用 postMessage 實現跨域通信

A頁面

// A頁面鏈接 http://localhost:8000/postMessage/A.html
<body>
  <iframe src="http://localhost:5000/postMessage/B.html" frameborder="0" id="iframeBox"></iframe>
  <script>
    let dataA = {
      code: 0,
      msg: 'A數據!'
    }
    iframeBox.onload = () => {
      iframeBox.contentWindow.postMessage(dataA, 'http://localhost:5000')
      window.onmessage = function (e) {
        console.log(e.data)
      }
    }
  </script>
</body>

B頁面

// B頁面鏈接 http://localhost:5000/postMessage/B.html
<body>
  <h3>B頁面請求同源接口獲取數據</h3>
  <script>
    let dataB = {
      code: 0,
      msg: 'B數據!'
    }
    window.onmessage = function (e) {
      console.log(e.data)
      e.source.postMessage(dataB, e.origin)
    }
  </script>
</body>

返回數據

3.8.2-1.png

4 總結

跨域方式 跨域分類 靜態資源服務器配合 數據服務器配合
jsonp JSOP
cors CORS
node request 代理
http proxy 代理
nginx 代理
window.name iframe 是(頁面)
document.domain iframe 是(頁面)
postMessage iframe 是(頁面)

5 主要參考資料

[1] 什么是跨域?跨域解決方法

[2] 瀏覽器為什么要設計同源策略?

[3] AJAX跨域訪問被禁止的原因

[4] 對于瀏覽器的同源策略你是怎樣理解的呢?

[5] JSONP

[6] axios

[7] cors實現請求跨域

[8] CORS on ExpressJS

[9 Request - Simplified HTTP client

[10] webpack配置proxy反向代理的原理是什么?

[11] webpack devServer.proxy

[12] Vue devServer.proxy

[13] React Proxying API Requests in Development

[14] nginx 之 proxy_pass詳解

[15] JS跨域--window.name

[16] Document.domain

[17] 利用window.postMessage()實現跨域消息傳遞

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容