一桃犬、什么是跨域,為什么會(huì)出現(xiàn)跨域問(wèn)題行楞。
前面兩篇文章cookie了解一下攒暇?,網(wǎng)絡(luò)攻防(xss/csrf/xsrf)了解一下介紹了在網(wǎng)站及網(wǎng)站用戶的安全問(wèn)題。瀏覽器為隔離潛在的惡意文件子房,限制了從同一個(gè)源加載的文檔或腳本如何與來(lái)自另一個(gè)源的資源進(jìn)行交互形用,所以,是瀏覽器的基于安全考慮的同源策略導(dǎo)致的跨域证杭。
同源(協(xié)議田度,域名,端口號(hào)三者均相同)解愤,只要有一樣不同則為跨域镇饺。
eg:IE比較特殊。
- IE瀏覽器不將端口劃在同源限制內(nèi)送讲,即:協(xié)議域名相同奸笤,只有端口不同惋啃,IE也認(rèn)為是同源的。
- 授信范圍(Trust Zones):兩個(gè)相互之間高度互信的域名监右,如公司域名(corporate domains)边灭,不遵守同源策略的限制。
二健盒、如何跨域(允許跨源訪問(wèn))
(1).JSONP
在HTML標(biāo)簽中绒瘦,一些標(biāo)簽比如:script,img這樣的src 是不受同源限制的味榛,可天然跨域椭坚。我們可以利用這一點(diǎn)來(lái)向后端請(qǐng)求數(shù)據(jù)。
思想: 動(dòng)態(tài)創(chuàng)建script標(biāo)簽搏色,將入?yún)⒑突卣{(diào)拼在url后面善茎,利用script標(biāo)簽的src去訪問(wèn),成功后刪除script標(biāo)簽
后端代碼
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async jsonp (ctx) {
// 前端傳過(guò)來(lái)的參數(shù)
const query = ctx.request.query
// 設(shè)置一個(gè)cookies
ctx.cookies.set('tokenId', '1')
// query.cb是前后端約定的方法名字频轿,其實(shí)就是后端返回一個(gè)直接執(zhí)行的方法給前端垂涯,由于前端是用script標(biāo)簽發(fā)起的請(qǐng)求,所以返回了這個(gè)方法后相當(dāng)于立馬執(zhí)行航邢,并且把要返回的數(shù)據(jù)放在方法的參數(shù)里耕赘。
ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`
}
}
module.exports = CrossDomain
前端代碼
/**
* JSONP請(qǐng)求工具
* @param url 請(qǐng)求的地址
* @param data 請(qǐng)求的參數(shù)
* @returns {Promise<any>}
*/
const request = ({url, data}) => {
return new Promise((resolve, reject) => {
// 處理傳參成xx=yy&aa=bb的形式
const handleData = (data) => {
const keys = Object.keys(data)
const keysLen = keys.length
return keys.reduce((pre, cur, index) => {
const value = data[cur]
const flag = index !== keysLen - 1 ? '&' : ''
return `${pre}${cur}=${value}${flag}`
}, '')
}
// 動(dòng)態(tài)創(chuàng)建script標(biāo)簽
const script = document.createElement('script')
// 接口返回的數(shù)據(jù)獲取
window.jsonpCb = (res) => {
document.body.removeChild(script)
delete window.jsonpCb
resolve(res)
}
script.src = `${url}?${handleData(data)}&cb=jsonpCb`
document.body.appendChild(script)
})
}
// 使用方式
request({
url: 'http://localhost:9871/api/jsonp',
data: {
// 傳參
msg: 'helloJsonp'
}
}).then(res => {
console.log(res)
})
特點(diǎn):
- 只能get請(qǐng)求:請(qǐng)求方式有限制关翎,安全性不高蛮粮,容易被攻擊忠荞,url有長(zhǎng)度限制肿仑,請(qǐng)求的入?yún)⑹芟藁鹧妫唧w各家瀏覽器不同
- 不能對(duì)請(qǐng)求頭進(jìn)行設(shè)置
- 需要后端配合
(2). iframe + form
基于jsonp不能發(fā)送post請(qǐng)求兽泄≌哂酰可以考慮在新的iframe中使用from來(lái)提交數(shù)據(jù)
思想:動(dòng)態(tài)創(chuàng)建iframe標(biāo)簽萎攒,結(jié)合form表單提交勒极。設(shè)置form.target = ifrme.name是掰。則form提交時(shí)會(huì)在該名稱的框架內(nèi)打開鏈接。但是form還是要追加到主文檔中辱匿。在iframe的load事件中處理返回的事件键痛。
后端接口代碼
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async iframePost (ctx) {
let postData = ctx.request.body
console.log(postData)
ctx.body = successBody({postData: postData}, 'success')
}
}
module.exports = CrossDomain
前端代碼
const requestPost = ({url, data}) => {
// 首先創(chuàng)建一個(gè)用來(lái)發(fā)送數(shù)據(jù)的iframe.
const iframe = document.createElement('iframe')
iframe.name = 'iframePost'
iframe.style.display = 'none'
document.body.appendChild(iframe)
const form = document.createElement('form')
const node = document.createElement('input')
// 注冊(cè)iframe的load事件處理程序,如果你需要在響應(yīng)返回時(shí)執(zhí)行一些操作的話.
iframe.addEventListener('load', function () {
console.log('post success')
})
form.action = url
// 在指定的iframe中執(zhí)行form
form.target = iframe.name
form.method = 'post'
for (let name in data) {
node.name = name
node.value = data[name].toString()
form.appendChild(node.cloneNode())
}
// 表單元素需要添加到主文檔中.
form.style.display = 'none'
document.body.appendChild(form)
form.submit()
// 表單提交后,就可以刪除這個(gè)表單,不影響下次的數(shù)據(jù)發(fā)送.
document.body.removeChild(form)
}
// 使用方式
requestPost({
url: 'http://localhost:9871/api/iframePost',
data: {
msg: 'helloIframePost'
}
})
特點(diǎn):
- 需要后端配合
- 通過(guò)iframe的load事件,返回結(jié)果處理不是很清晰
(3). CORS
CORS是一個(gè)W3C標(biāo)準(zhǔn)匾七,全稱是”跨域資源共享”(Cross-origin resource sharing)跨域資源共享 CORS 詳解需要瀏覽器和服務(wù)器同時(shí)支持
-
簡(jiǎn)單請(qǐng)求
后端接口配置// 處理成功失敗返回格式的工具 const {successBody} = require('../utli') class CrossDomain { static async cors (ctx) { const query = ctx.request.query // *時(shí)cookie不會(huì)在http請(qǐng)求中帶上 ctx.set('Access-Control-Allow-Origin', '*') ctx.cookies.set('tokenId', '2') ctx.body = successBody({msg: query.msg}, 'success') } } module.exports = CrossDomain
核心就是'Access-Control-Allow-Origin設(shè)置為 * 表示允許所有的遠(yuǎn)程訪問(wèn)該資源絮短,所以前端就什么也不用配置,直接請(qǐng)求后臺(tái)接口即可昨忆。但是當(dāng)設(shè)置為*的時(shí)候http頭是不會(huì)攜帶cookie的戚丸,所以,如果要攜帶cookie,則前后端都要設(shè)置一下
2.非簡(jiǎn)單請(qǐng)求
后端接口
// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
static async cors (ctx) {
const query = ctx.request.query
// 如果需要http請(qǐng)求中帶上cookie限府,需要前后端都設(shè)置credentials夺颤,且后端設(shè)置指定的origin
ctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')
ctx.set('Access-Control-Allow-Credentials', true)
// 非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求,會(huì)在正式通信之前胁勺,增加一次HTTP查詢請(qǐng)求世澜,稱為"預(yù)檢"請(qǐng)求(preflight)
// 這種情況下除了設(shè)置origin,還需要設(shè)置Access-Control-Request-Method以及Access-Control-Request-Headers
ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
ctx.cookies.set('tokenId', '2')
ctx.body = successBody({msg: query.msg}, 'success')
}
}
module.exports = CrossDomain
前端請(qǐng)求代碼
fetch(`http://localhost:9871/api/cors?msg=helloCors`, {
// 需要帶上cookie
credentials: 'include',
// 這里添加額外的headers來(lái)觸發(fā)非簡(jiǎn)單請(qǐng)求
headers: {
't': 'extra headers'
}
}).then(res => {
console.log(res)
})
(4). 代理
想一下署穗,如果我們請(qǐng)求的時(shí)候還是用前端的域名寥裂,然后有個(gè)東西幫我們把這個(gè)請(qǐng)求轉(zhuǎn)發(fā)到真正的后端域名上,不就避免跨域了嗎案疲。
nginx代理
server{
# 監(jiān)聽9099端口
listen 9099;
# 域名是localhost
server_name localhost;
#凡是localhost:9099/api這個(gè)樣子的封恰,都轉(zhuǎn)發(fā)到真正的服務(wù)端地址http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
前端開發(fā)時(shí)的dev-server代理
webpackConfig.devServer = {
host: '0.0.0.0', //加上這個(gè)配置才能讓別人訪問(wèn)你的本地服務(wù)器
contentBase: './dist', //本地服務(wù)器所加載的頁(yè)面所在的目錄
port: 8888,
historyApiFallback: true, //不跳轉(zhuǎn)
inline: true, //實(shí)時(shí)刷新
//代理到j(luò)son-server的端口,模擬后端接口
proxy: {
'/api/*': {
target: 'http://localhost:8787',
secure: false,
changeOrigin: true,
pathRewrite: {
'^/api/': '/'
},
}
}
};
(5). 設(shè)置瀏覽器快捷方式的 --args --disable-web-security --user-data-dir
同源策略限制DOM查詢下的 跨域方式
一褐啡、window.postMessage
postMessage是HTML5的一個(gè)新特性诺舔,專門用來(lái)解決跨域的方法,但是目前es6,es7不支持备畦。postMessage方法允許來(lái)自不同源的腳本低飒,采用異步的方式進(jìn)行通信,可以實(shí)現(xiàn)跨文本檔懂盐,多窗口褥赊,跨域消息傳遞。
- 跨域除了客戶端和服務(wù)端請(qǐng)求的接口跨域莉恼,還有:
1)多窗口之間消息傳遞(newWin = window.open(..));
2)頁(yè)面與嵌套的iframe消息傳遞
postMessage跨域不是解決客戶端與服務(wù)端的跨域問(wèn)題拌喉,而是專注解決,客戶端中跨文檔消息傳送通信俐银。
2.向目標(biāo)窗口傳送消息
postMessage(dataStr,origin)方法接受兩個(gè)參數(shù)司光;dataStr是要傳送的消息字符串,origin是目標(biāo)窗口的源(協(xié) 議悉患,域名,端口)榆俺。
-
目標(biāo)窗口接收消息
監(jiān)聽message事件window.addEventListener('message', function(messageEvent) { var data = messageEvent.data; // messageEvent: {source, currentTarget, data} console.info('message from child:', data); }, false);
接收消息
window.addEventListener('message', (e) => { // 這里一定要對(duì)來(lái)源做校驗(yàn) if (e.origin === 'http://localhost:9099') { // http://localhost:9099發(fā)來(lái)的信息 console.log(e.data) // e.source可以是回信的對(duì)象售躁,其實(shí)就是http://localhost:9099窗口對(duì)象(window)的引用 // e.origin可以作為targetOrigin e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟茴晋,這就是你想知道的結(jié)果:${document.getElementById('app') ? '有id為app的Dom' : '沒有id為app的Dom'}`, e.origin); } })
e事件對(duì)象有三個(gè)屬性
- source:發(fā)送消息的窗口對(duì)象
- origin:發(fā)送消息窗口的源(協(xié)議+主機(jī)+端口號(hào))
- data:顧名思義陪捷,是傳遞來(lái)的message
二、document.domain
這種方式只適合主域名相同诺擅,但子域名不同的iframe跨域市袖。
比如主域名是crossdomain.com:9099,子域名是child.crossdomain.com:9099,這種情況下給兩個(gè)頁(yè)面指定一下document.domain即document.domain = crossdomain.com就可以訪問(wèn)各自的window對(duì)象了苍碟。
三酒觅、canvas操作圖片的跨域問(wèn)題
這個(gè)應(yīng)該是一個(gè)比較冷門的跨域問(wèn)題,張大神已經(jīng)寫過(guò)了我就不再班門弄斧了解決canvas圖片getImageData,toDataURL跨域問(wèn)題