關於跨域及其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的實現方案
- 建立script標籤,src屬性指向外部資源
- 在src上指定回撥函式,並將json資料攜帶在回撥函式的引數上響應給客戶端
- 客戶端執行回撥函式,獲取資料
方式一: 本地開啟服務,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
實現:
- 通過iframe巢狀需要傳送資訊的網頁,並監聽onload事件保證網頁載入完畢
- 在load回撥裡通過iframe.contentWindow獲取對方網頁,使用iframe.contentWindow.
- postMessage向對方網頁傳送資訊,對方網頁通過window監聽onmessage獲取資訊
- 對方網頁在onmessage回撥中,e.source.postMessage向我放網頁傳送資訊
- 我方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
思路:
- a通過iframe引用c,並在url後面設定hash值hasha,監聽hashchange事件
- c用loaction.hash收到hasha之後,通過iframe引用和a同源的b,並在iframe的src上設定hash值hashc
- 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