項目源碼: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
、LocalStorage
和IndexedDB
- 無法接觸非同源網頁的
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>
客戶端數據請求結果
同源請求下,得到服務器返回數據
{"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
讀取另一個域名的內容。但瀏覽器并不阻止向另一個域名發送請求。
跨域請求(非同源策略請求)限制為瀏覽器限制,非服務器限制。無論是否是跨域請求,服務器均會返回數據。
2 為什么會有跨域限制
-
保護cookie、LocalStorage 和 IndexedDB
cookie
中存著sessionID
(登錄憑證)。當用戶訪問惡意網站時,如果沒有同源策略,那么這個網站就可以通過js
訪問document.cookie
得到用戶關于各個網站的sessionID
,如果這個sessionID
在有效期內,惡意網站就可以利用sessionID
登錄各個網站,獲取用戶其他信息。[2]
-
保護DOM操作
惡意網站通過
iframe
加載支付寶頁面,當用戶進入惡意網站后,誤以為是支付寶官方頁面,輸入用戶名、密碼等信息。如果沒有同源策略,惡意網站就可以通過DOM操作
獲取到用戶輸入值,從而控制用戶賬戶。[2]
-
限制 ajax操作
cookie 工作機制
客戶向 A 網站的服務器發送登錄請求,并攜帶賬號密碼數據
A 網站的服務器校驗賬號密碼正確后,返回響應并給本地添加了cookie
之后客戶再次向 A 網站發起請求會自動帶上A網站存儲在本地的cookie
A 網站的服務器從cookie中獲取賬號密碼數據后,返回登陸成功界面
2-1.png
用戶登錄過支付寶后,支付寶在本地設置了 cookie
信息。如果沒有同源策略,當用戶訪問惡意網站時,惡意網站利用存儲在本地的 cookie
等信息通過 ajax
向支付寶發起登錄請求,從 ajax
回調中解析到用戶數據信息。[3]
3 八種跨域解決方案
3.1 JSONP
3.1.1 JSONP 可用前提
瀏覽器安全性和方便性是成反比的,十位數的密碼提高了安全性,但是不方便記憶。同樣,同源策略提升了 Web
前端的安全性,但犧牲了Web拓展上的靈活性。
設想若把 html
、js
、css
、flash
,image
等文件全部布置在一臺服務器上,小型網站這樣還可以,大中型網站如果這樣做服務器無法承受。為解決服務器冗余,在實型前后端分離,靜態資源服務器、圖片資源服務器、視頻資源服務器等拆分后,雖然系統變的更加靈活,但受制于瀏覽器同源策略限制,不同服務器之間通信受到限制。雖然保證了安全,Web
方便性大打折扣。
所以,現代瀏覽器在安全性和可用性之間選擇了一個平衡點。在遵循同源策略的基礎上,選擇性地為同源策略“開放了后門”。 例如 script
、 img
、link
、iframe
等標簽,都允許垮域引用資源,嚴格說這都是不符合同源要求的。(當然,用戶只能是引用這些資源而已,并不能讀取這些資源的內容。例如在自己域內可以讀取百度 logo
圖片,但無法讀取到該數據的二進制資源。)[4]
利用瀏覽器允許 script
標簽跨域引用資源的特性,形成了一種非正式傳輸協議,JSONP
。
3.1.2 一般 JSONP 使用方法及其實現原理
JSONP
( JSON 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.2 JSONP 實現跨域請求原理
注意:客戶端定義的必須是全局函數,因為瀏覽器中收到服務器返回的函數執行只有在全局函數下才能運行。
3.1.3 ajax 下使用 JSONP及其原理
JSONP
是非官方跨域數據交互協議,但 ajax
對其進行了封裝,可采用 ajax
發起 jsonp
跨域請求。(axios
無 jsonp
請求方式)
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.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.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 跨域配置方式
服務器未配置時限制跨域
由圖中可以看出,瀏覽器不允許跨域原因已指出,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.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', '*')
,瀏覽器會報錯(因為任何域都攜帶憑證請求會影響安全)。只允許設置域形式。
同樣,若 Access-Control-Allow-Origin
設置多個 域,即 res.header('Access-Control-Allow-Origin', 'http://localhost:8000, http://localhost:8001')
,瀏覽器會報錯。
即在客戶端攜帶憑證請求情況下只能設置允許一個域的請求。 7
8
未設置 withCredentials
為 true
時的請求頭(無 cookie
)
設置 withCredentials
為 true
時的請求頭(有 cookie
)
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 作為中間件代理
Nodejs
的 request
模塊是服務端發起請求的工具包,可在本服務器向其他域的服務器發起請求。使用前需安裝 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.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.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.3 react 中 proxy 設置
簡單配置
可在 package.json
中進行簡單配置,這樣配置后全局接口都會被代理到 http://localhost:5000
。13
"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.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>
客戶端服務器配置
可在 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.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>
此時,在同一瀏覽器窗口,將鏈接改為非同源的 A頁面(http://localhost:8000/window.name/A.html),發現此時仍可以通過 window.name 拿到數據。
利用這一點,可以試圖在 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.2 利用 window.name 進行跨域
因為 A頁面 與 C頁面 一直存在于同一瀏覽器窗口內,window.name
值一直存在,所以可以使用無任何內容的 中間頁面 proxy.html
(與 A頁面 同源),在 C頁面 加載后,將 iframe
的 src
值更改為中間頁面 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.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.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){ }
參數 e
為 message
實例,里面包含了 data
、origin
、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>
返回數據
4 總結
跨域方式 | 跨域分類 | 靜態資源服務器配合 | 數據服務器配合 |
---|---|---|---|
jsonp | JSOP | 否 | 是 |
cors | CORS | 否 | 是 |
node request | 代理 | 是 | 否 |
http proxy | 代理 | 是 | 否 |
nginx | 代理 | 是 | 否 |
window.name | iframe | 否 | 是(頁面) |
document.domain | iframe | 否 | 是(頁面) |
postMessage | iframe | 否 | 是(頁面) |
5 主要參考資料
[1] 什么是跨域?跨域解決方法
[2] 瀏覽器為什么要設計同源策略?
[3] AJAX跨域訪問被禁止的原因
[5] JSONP
[6] axios
[7] cors實現請求跨域
[9 Request - Simplified HTTP client
[10] webpack配置proxy反向代理的原理是什么?
[12] Vue devServer.proxy
[13] React Proxying API Requests in Development
[14] nginx 之 proxy_pass詳解
[15] JS跨域--window.name
[16] Document.domain