跨域旦万、CORS闹击、JSONP(面試必考)
ajax和Promise的跨域問(wèn)題
跨域一般用2種方案JSONP和CORS
跨域
前端與后端的交互。
關(guān)鍵知識(shí)
1.同源策略 瀏覽器故意設(shè)計(jì)的一個(gè)功能限制
2.CORS 突破瀏覽器限制的一個(gè)辦法
3.JSONP IE時(shí)代的妥協(xié)
一.同源策略
1.同源定義
源
控制臺(tái)輸入window.origin或location.origin可以得到當(dāng)前源
源=協(xié)議+域名+端口號(hào)
如果兩個(gè)url的協(xié)議成艘、域名赏半、端口號(hào)完全一致,那么這兩個(gè)url就是同源的淆两。
舉例
https://qq.com断箫、https://www.baidu.com不同源
https://baidu.com、https://www.baidu.com不同源
完全一致才算同源
2.同源策略定義
瀏覽器規(guī)定
如果JS運(yùn)行在源A里就只能獲取源A的數(shù)據(jù)秋冰,不能獲取源B的數(shù)據(jù)仲义,即不允許跨域。
舉例(省略http協(xié)議)
假設(shè)diaoyu.com/index.html引用了cdn.com/1.js
那么就說(shuō)1.js運(yùn)行在源diaoyu.com里剑勾。注意這跟cdn.com沒(méi)關(guān)系埃撵,雖然1.js從它那下載
所以1.js就只能獲取diaoyu.com的數(shù)據(jù)
1.js運(yùn)行在哪個(gè)域名里就只能訪問(wèn)這個(gè)域名的數(shù)據(jù)
這是瀏覽器的功能,瀏覽器故意要這樣設(shè)計(jì)的虽另。
瀏覽器這么做的目的是為了保護(hù)用戶隱私暂刘。
怎么保護(hù)的?(假設(shè)沒(méi)有同源策略,所有的js可以訪問(wèn)所有的網(wǎng)站的數(shù)據(jù))
以QQ空間為例
源為https://user.qzone.qq.com
假設(shè)捂刺,當(dāng)前用戶已登陸(登陸信息保存在瀏覽器Cookie里)谣拣,現(xiàn)在可以查看好友列表募寨。
好友信息是通過(guò)Ajax請(qǐng)求/friends.json獲取到的好友列表
假設(shè),Ajax請(qǐng)求/friends.json可獲取用戶好友列表
目前為止都很正常
黑客來(lái)了
假設(shè)你的女神(黑客)分享https://qzone-qq.com 給你森缠,實(shí)際是釣魚(yú)網(wǎng)站拔鹰。
你點(diǎn)開(kāi)這個(gè)網(wǎng)頁(yè),這個(gè)網(wǎng)頁(yè)也請(qǐng)求你的好友列表
https://user.qzone.qq.com/friends.json
請(qǐng)問(wèn)贵涵,你的好友列表是不是就被黑客偷走了列肢?是的
問(wèn)題的根源
第1次是正常的請(qǐng)求,第2次是黑客的請(qǐng)求独悴。幾乎沒(méi)有區(qū)別除了referrer
控制臺(tái)的XHR是XMLHttpRequest的縮寫(xiě)也就是ajax的縮寫(xiě)
同源策略:不同源的頁(yè)面之間例书,不準(zhǔn)互相訪問(wèn)數(shù)據(jù)
實(shí)踐 完整代碼
做2個(gè)網(wǎng)站來(lái)演示下
步驟
1.創(chuàng)建2個(gè)目錄qq-com和diaoyu-com
qq-com目錄新建server.js,模擬QQ空間
diaoyu-com目錄新建server.js,模擬壞人空間
先打開(kāi)第1個(gè)目錄锣尉,然后將第2個(gè)目錄添加到工作區(qū)
安裝:yarn global add node-dev
使用:node-dev server.js 8888 //qq-com
node-dev server.js 9999 //diaoyu-com
2.qq-com
/index.html是首頁(yè)
/qq.js是JS腳本文件
/friends.json是模擬的好友數(shù)據(jù)
端口監(jiān)聽(tīng)為8888刻炒,訪問(wèn)http://127.0.0.1:8888
操作步驟
編寫(xiě)path='/index.html'路由,然后新建index.html。server.js會(huì)讀取index.html自沧。
編寫(xiě)path === '/qq.js'路由,然后新建qq.js坟奥。server.js會(huì)讀取qq.js。
編寫(xiě)path === '/friends.json'路由,然后新建friends.json拇厢。server.js會(huì)讀取friends.json爱谁。
知識(shí)點(diǎn):fs文檔、fs模塊-阮一峰推薦網(wǎng)道
Node.js文件系統(tǒng)封裝在fs模塊中孝偎,它提供了文件的讀取访敌、寫(xiě)入、更名衣盾、刪除寺旺、遍歷目錄、鏈接等POSIX文件系統(tǒng)操作
fs模塊的基本使用:
var fs = require('fs') //引用fs
response.write(fs.readFileSync('./public/index.html'))
3.diaoyu-com
操作步驟同上
/index.html是首頁(yè)
/diaoyu.js是JS腳本文件
補(bǔ)充:diaoyu-com不需要寫(xiě)路由/friends.json
端口監(jiān)聽(tīng)為9999势决,訪問(wèn)http://127.0.0.1:9999
4.讓qq.js成功訪問(wèn)自己的json(friends.json)數(shù)據(jù)
讓diaoyu.js不能成功訪問(wèn)qq的json
能不能做到域名也不同?設(shè)置本地域名映射,修改hosts
讓qq.com映射到127.0.0.1,就可以訪問(wèn) http://qq.com:8888/index.html 了
讓diaoyu.com映射到127.0.0.1,就可以訪問(wèn) http://diaoyu.com:9999/index.html 了
跨域AJAX
演示跨域被阻止
1.qq-com
首先index.html應(yīng)用js<script src="qq.js"></script>
然后qq.js發(fā)送請(qǐng)求
const request = new XMLHttpRequest()
request.open('GET', './friends.json')
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
console.log(request.response)
}
}
request.send()
2.diaoyu-com
操作同上
首先index.html應(yīng)用js<script src="./diaoyu.js"></script>
然后qq.js發(fā)送請(qǐng)求
const request = new XMLHttpRequest()
request.open('GET', 'http://qq.com:8888/friends.json')//請(qǐng)求qq.com的json
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
console.log(request.response)
}
}
request.send()
diaoyu-com不需要/friends.json路由
提示:從源“http://diaoyu.com:9999”訪問(wèn)“http://qq.com:8888/friends.json”的XMLHttp請(qǐng)求已被CORS策略阻止:請(qǐng)求的資源上不存在“訪問(wèn)-控制-允許源”頭阻塑。
response拿不到qq.com的數(shù)據(jù),因?yàn)?strong>CORS策略阻止了果复。
這就是跨域陈莽,跨域會(huì)被阻止的。瀏覽器會(huì)限制跨域虽抄,故意不給數(shù)據(jù)走搁。瀏覽器需要CORS。
其它疑問(wèn)
1.為什么a.qq.com訪問(wèn)qq.com也算跨域迈窟?
因?yàn)闅v史上出現(xiàn)過(guò)不同公司共用域名私植。a.qq.com和qq.com不一定是同一個(gè)網(wǎng)站。瀏覽器謹(jǐn)慎起見(jiàn)菠隆,認(rèn)為這是不同的源兵琳。
2.為什么不同端口也算跨域狂秘?
原因同上。曾經(jīng)由于服務(wù)器比較貴躯肌,很多公司共用同一個(gè)服務(wù)器者春,這導(dǎo)致了一個(gè)端口一個(gè)公司,這兩個(gè)公司數(shù)據(jù)是不共享的清女。記住安全鏈條的強(qiáng)度取決于最弱的一環(huán)钱烟,任何安全相關(guān)的問(wèn)題都要謹(jǐn)慎對(duì)待。
3.為什么兩個(gè)網(wǎng)站的IP是一樣的嫡丙,也算跨域拴袭?
原因同上。IP都一樣說(shuō)明在同一臺(tái)服務(wù)器上曙博。很早的時(shí)候服務(wù)器很貴的拥刻,所以一臺(tái)服務(wù)器掛了20個(gè)不同網(wǎng)站的網(wǎng)頁(yè)都是有可能的。所以由于IP可以共用父泳,實(shí)際上它們是不同的公司般哼。
4.為什么可以跨域使用CSS、JS惠窄、圖片蒸眠?
面試題
同源策略限制的是數(shù)據(jù)訪問(wèn),我們引用CSS杆融、JS楞卡、圖片的時(shí)候,其實(shí)并不知道其內(nèi)容脾歇,我們只是在引用蒋腮。能引用但不能讀取。不信我問(wèn)你介劫,你能知道CSS的第1個(gè)字符是什么嗎徽惋?
雖然理論上要求不同網(wǎng)站不能共享數(shù)據(jù),不允許跨域座韵,但實(shí)際工作或者面試中經(jīng)常會(huì)問(wèn)如何跨域共享數(shù)據(jù)险绘。
實(shí)現(xiàn)跨域的2種方法
方法一:CORS 跨域資源共享
問(wèn)題根源
瀏覽器默認(rèn)不同源之間不能互相訪問(wèn)數(shù)據(jù)。
但是qq.com和diaoyu.com其實(shí)都是我的網(wǎng)站,我就是想要兩個(gè)網(wǎng)站互相訪問(wèn)誉碴,瀏覽器為什么阻止宦棺。
好吧,用CORS
瀏覽器說(shuō)黔帕,如果要共享數(shù)據(jù)代咸,需要提前聲明!
怎么聲明成黄?
瀏覽器說(shuō)呐芥,qq.com在響應(yīng)頭里寫(xiě)diaoyu.com可以訪問(wèn)
設(shè)置響應(yīng)頭Access-Control-Allow-Origin: http://foo.example
CORS文檔 CORS分為簡(jiǎn)單請(qǐng)求和復(fù)雜請(qǐng)求具體看文檔
示例代碼
else if (path === '/friends.json') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/json;charset=utf-8')
response.setHeader('Access-Control-Allow-Origin', 'http://diaoyu.com:9999')
response.write(fs.readFileSync('./public/friends.json'))
response.end()
}
如果有2個(gè)網(wǎng)站都想請(qǐng)求qq.com呢逻杖?
思路:來(lái)一個(gè)網(wǎng)站讀取一個(gè)網(wǎng)站,不是將這2個(gè)網(wǎng)站都寫(xiě)上去。
console.log(request.headers['referer'])
將當(dāng)前正在請(qǐng)求的網(wǎng)站記錄下來(lái)思瘟,然后重新記錄到Access-Control-Allow-Origin后面
??注意:IE不支持CORS,要想兼容IE只能用第2種方法
方法二:JSONP(兼容IE)
JSONP和JSON半毛錢(qián)關(guān)系都沒(méi)有
記不記得我們可以任意引用JS
雖然我們不能訪問(wèn)qq.com:8888/friends.json荸百,但是我們能引用qq.com:8888friends.js啊滨攻!
這有什么用够话?
JS又不是數(shù)據(jù)
我們讓JS包含數(shù)據(jù)不就好了,把數(shù)據(jù)寫(xiě)到j(luò)s文件里
qq創(chuàng)建包含數(shù)據(jù)的js光绕,diaoyu.com訪問(wèn)qq.com
步驟
1.qq.com將數(shù)據(jù)寫(xiě)到/friends.js
qq新建文件friends.js,代碼
{ { data } } //占位
2.后臺(tái)寫(xiě)路由
后臺(tái)獲取js內(nèi)容女嘲、獲取數(shù)據(jù)json,把數(shù)據(jù)填到j(luò)s內(nèi)容诞帐。然后再返回給瀏覽器欣尼。
else if (path === '/friends.js') {
response.statusCode = 200
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
const string = fs.readFileSync('./public/friends.js').toString()
const data = fs.readFileSync('./public/friends.json').toString()
//默認(rèn)不是string要toString
const string2 = string.replace('{{data}}', data)
//把data塞到string里。用數(shù)據(jù)data把占位{{data}}替換掉
response.write(string2)
response.end()
}
補(bǔ)充:js文件中光有數(shù)據(jù)是不合法的需要賦值
window.xxx = { { data } }
3.diaoyu.com用script標(biāo)簽引用/friends.js,js動(dòng)態(tài)引用
diaoyu.js
const script = document.createElement('script')
script.src = 'http://qq.com:8888/friends.js'
document.body.appendChild(script)
那怎么確定數(shù)據(jù)是否拿到呢景埃?監(jiān)聽(tīng)script的onload事件
const script = document.createElement('script')
script.src = 'http://qq.com:8888/friends.js'
script.onload = () => {
console.log(window.xxx)
}
document.body.appendChild(script)
4.js文件中光有數(shù)據(jù)是不合法的需要賦值,但是xxx可能被占用媒至,可以用函數(shù)
//window.xxx={{data}}
window.xxx({{ data }})
diaoyu.js
不用再監(jiān)聽(tīng)onload了。因?yàn)橹灰獔?zhí)行成功就可以調(diào)用函數(shù)window.xxx了
window.xxx = (data) => {
console.log(data)
}
const script = document.createElement('script')
script.src = 'http://qq.com:8888/friends.js'
document.body.appendChild(script)
回調(diào):我定義一個(gè)函數(shù)給別人調(diào)用谷徙,別人調(diào)的時(shí)候給我個(gè)數(shù)據(jù)。
window.xxx本質(zhì)就是一個(gè)回調(diào)
我定義了但是不調(diào)用驯绎,等著qq.com的腳本js文件(friends.js)來(lái)調(diào)用完慧。調(diào)的時(shí)候就會(huì)把數(shù)據(jù)作為第1個(gè)參數(shù)傳給我。
JSONP referer檢查限制訪問(wèn)者
CORS可以指定誰(shuí)訪問(wèn)者剩失,JSONP沒(méi)辦法指定屈尼,那JSONP的js不是所有網(wǎng)站都可以用了嗎?
是的拴孤,但是JSONP可以做referer檢查限制訪問(wèn)者脾歧。
else if (path === '/friends.js') {
if (request.headers['referer'].indexOf('http://diaoyu.com:9999') === 0) {
response.statusCode = 200
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
//console.log(request.headers['referer']) //http://diaoyu.com:9999
const string = fs.readFileSync('./public/friends.js').toString()
const data = fs.readFileSync('./public/friends.json').toString()
const string2 = string.replace('{{ data }}', data)
response.write(string2)
response.end()
} else {
response.statusCode = 404
response.end()
}
}
indexOf()
即使這樣還是會(huì)有安全隱患,一旦我信任的網(wǎng)站被攻陷(diaoyu.com)演熟,那qq.com也就完了鞭执。
還有更嚴(yán)格的限制以后會(huì)講(涉及Cookie、Token)
優(yōu)化JSONP
一.xxx能不寫(xiě)死嗎芒粹?
假如我有多個(gè)接口兄纺,除了"好友列表",還有"最近訪問(wèn)的人"等化漆,這明顯不能滿足我們的需求估脆。其實(shí)名字不重要,只要diaoyu.com定義的函數(shù)名和qq.com/friends.js執(zhí)行的函數(shù)名是同一個(gè)即可座云。那就把名字傳給/friends.js
1.思路:自動(dòng)生成
const random = Math.random()
//console.log(random) 0~1的小數(shù)疙赠,所有的key都可以是字符串付材,只要是字符串就行
window[random] = (data) => { //window['0.023216364']=函數(shù)
console.log(data)
}
2.如何把隨機(jī)數(shù)給后臺(tái)?查詢參數(shù)?functionName
diapyu.js
script.src = `http://qq.com:8888/friends.js?functionName=${random}`
傳進(jìn)去后怎么渲染到j(luò)s里?
qq.com
else if (path === '/friends.js') {
if (request.headers['referer'].indexOf('http://diaoyu.com:9999') === 0) {
...
console.log(query.functionName) //query?
}
}
補(bǔ)充 url.parse
var parsedUrl = url.parse(request.url, true)
var query = parsedUrl.query
語(yǔ)法:第2個(gè)參數(shù)(可省)傳入一個(gè)布爾值圃阳,默認(rèn)為false伞租,為true時(shí),返回的url對(duì)象中限佩,query的屬性為一個(gè)對(duì)象葵诈。
url.parse("http://user:pass@host.com:8080/p/a/t/h?query=string#hash",true);
返回值:
{
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: { query: 'string' },
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
}
將第2個(gè)參數(shù)設(shè)置為true時(shí),query屬性為 “名稱/值”對(duì)的集合,即json格式祟同。
將隨機(jī)數(shù)插入到字符串里
friends.js
window['{{xxx}}']({{ data }})
再將{{xxx}}替換成隨機(jī)數(shù)
const string2=string.replace('{{ data }}', data).replace('{{xxx}}',query.functionName)
還可以更復(fù)雜點(diǎn):加個(gè)前綴
const random = 'diaoyuJSONPCallbackName' + Math.random()
二.執(zhí)行完后刪掉<script>
標(biāo)簽
每發(fā)一次請(qǐng)求就會(huì)增加一個(gè)<script>
標(biāo)簽作喘,頁(yè)面也會(huì)變的臃腫。
const random = 'diaoyuJSONPCallbackName' + Math.random()
console.log(random)
window[random] = (data) => {
console.log(data)
}
const script = document.createElement('script')
script.src = `http://qq.com:8888/friends.js?functionName=${random}`
script.onload = () => { //刪掉<script>標(biāo)簽
script.remove()
}
document.body.appendChild(script)
只要執(zhí)行完了晕城,<script>
就沒(méi)用了泞坦,可以刪掉
三.封裝jsonp()
封裝成jsonp('url').then(f1,f2)
function jsonp(url) {
return new Promise((resolve, reject) => {
const random = 'diaoyuJSONPCallbackName' + Math.random()
window[random] = (data) => {//成功調(diào)用resolve
resolve(data)
}
const script = document.createElement('script')
//script.src = `${url}?functionName=${random}`
script.src = `${url}?callback=${random}`
script.onload = () => {
script.remove()
}
script.onerror = () => {//失敗調(diào)用reject
reject()
}
document.body.appendChild(script)
})
}
jsonp('http://qq.com:8888/friends.js').then((data) => {
console.log(data)
})
jsonp與ajax對(duì)比的弱點(diǎn),jsonp只能知道成功/失敗砖顷,不能拿到狀態(tài)碼贰锁。
??注意:jsonp約定查詢參數(shù)不能叫functionName應(yīng)該叫callback?callback
四.刪除文件friends.js
friends.js的window['{{xxx}}']({{ data }})
這句代碼其實(shí)可以直接寫(xiě)在后臺(tái)。
//const string = fs.readFileSync('./public/friends.js').toString()
const string = `window['{{xxx}}']({{ data }})`
JSONP就是創(chuàng)建一個(gè)<script>
請(qǐng)求js滤蝠,js把數(shù)據(jù)夾帶過(guò)來(lái)
跨域還有其他方案可以自己搜
面試題
JSONP是什么豌熄?
1.跨域時(shí)由于當(dāng)前瀏覽器不支持CORS,所以必須使用另外一種方式實(shí)現(xiàn)跨域物咳。
于是我們就請(qǐng)求一個(gè)js文件锣险,這個(gè)js文件會(huì)執(zhí)行一個(gè)回調(diào),回調(diào)里有我們的數(shù)據(jù)览闰。
追問(wèn)芯肤,你這個(gè)回調(diào)的名字是什么?
回調(diào)的名字是可以隨機(jī)生成的1個(gè)隨機(jī)數(shù)压鉴,我們把這個(gè)名字以callback的參數(shù)傳給后臺(tái)崖咨,后臺(tái)會(huì)把這個(gè)函數(shù)再次返回給我們并執(zhí)行。
2.JSONP優(yōu)點(diǎn):(1)兼容IE (2)可以跨域
3.JSONP缺點(diǎn):
(1)由于是<script>
標(biāo)簽,所以它讀不到ajax那么精確的狀態(tài)油吭。狀態(tài)碼击蹲、響應(yīng)頭都不知道,只知道成功和失敗上鞠。
(2)由于是<script>
標(biāo)簽只能發(fā)get請(qǐng)求,JSONP不支持post际邻。