簡單易懂的JSONP和CORS跨域方案詳解
一、了解跨域
??何為跨域
- 跨域并不是請求發(fā)不出去,請求能發(fā)出去,服務(wù)端能收到請求并正常返回結(jié)果,只是結(jié)果被瀏覽器攔截了
那為什么攔截呢,我們應(yīng)該先了解一下什么是同源策略
- MDN上的解釋:同源策略是一個(gè)重要的安全策略,它用于限制一個(gè)origin的文檔或者它加載的腳本如何能與另一個(gè)源的資源進(jìn)行交互。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介
具體的限制(三限制):
- 同源策略限制了來自不同源的 JavaScript 腳本對當(dāng)前DOM對象讀和寫的操作
- 同源策略限制了來自不同源的站點(diǎn)讀取當(dāng)前的 Cookie、LocalStorage 等數(shù)據(jù)
- 同源策略限制了通過 XMLHttpRequest等方式將站點(diǎn)的數(shù)據(jù)發(fā)送給不同源的站點(diǎn)
能使的標(biāo)簽(能跨域請求資源的標(biāo)簽):
- <script src="..."></script>
- <link rel="stylesheet" href="...">
- 通過 <img>展示圖片
- 通過 <video>和 <audio>播放的多媒體資源
- 通過 <iframe>載入的任何資源
那你限制了我這么多東西,我要正常使用肯定要跨域嘍
??何為同源
如果兩個(gè) URL 的協(xié)議(Protocol),域名(domain),端口(port)都相同的話,則這兩個(gè) URL 是同源
- 協(xié)議:定義了數(shù)據(jù)如何在計(jì)算機(jī)內(nèi)和之間進(jìn)行交換的規(guī)則的系統(tǒng)。例如:http,https
- 域名:可以通過DNS解析為IP地址 (這里即使兩個(gè)不同的域名指向同一個(gè)IP地址,也是屬于不同源的)
- 端口:http默認(rèn)使用80端口,https默認(rèn)使用443端口
舉個(gè)例子: 我們尋找跟掘金首頁:https://jbzj.cn/同源的URL
URL | 結(jié)果 | 原因 |
---|---|---|
http://jbzj.cn/ | 不同源 | 協(xié)議不同 |
https://jbzj.my.cn/ | 不同源 | 域名不同 |
https://jbzj.cn:3030/ | 不同源 | 端口不一樣(https默認(rèn)443端口) |
https://jbzj.cn/user/2375390426313406 | 同源 | 只有路徑不一樣 |
https://jbzj.cn/post/7151609034699718692 | 同源 | 只有路徑不一樣 |
小結(jié):跨域的出現(xiàn)是因?yàn)闉g覽器的同源策略對不同協(xié)議,域名,端口的URL在DOM對象的讀寫,Cookie等數(shù)據(jù)的獲取,Ajax請求等方面有限制
二、跨域解決方案
JSONP(JSON with Padding)
- 最容易記住也最容易理解的應(yīng)該就是JSONP了
- 我們前面提到了<script src="..."></script>是可以跨域請求資源的,那么JSONP就是利用了這一點(diǎn)了
??簡單實(shí)現(xiàn)
- 先上一個(gè)簡單的例子來理解思路
- 在客戶端
<script> function jsonp(){ return new Promise((resolve, reject) => { //在全局定義一個(gè)doSomething的函數(shù) window['doSomething'] = function(data) { resolve(data); } }) } jsonp().then(res=>{ //調(diào)用doSomething之后會(huì)在這里打印 console.log(res) }) </script> //使用script標(biāo)簽向后臺(tái)/test路徑發(fā)送參數(shù)callback為doSomething <script src="http://localhost:3000/test?callback=doSomething"></script>
- 在服務(wù)端
app.get('/test',(req, res) => { //通過res.send()方法-將處理好的內(nèi)容,發(fā)送給客戶端 let { callback } = req.query; //返回一個(gè)函數(shù)的執(zhí)行式doSomething(‘參數(shù)') res.send(`${callback}('這里是某車在使用簡單的JSONP')`) })
- 服務(wù)器返回doSomething('這里是某車在使用簡單的JSONP'),因?yàn)槲覀円呀?jīng)聲明過,所以客戶端會(huì)直接調(diào)用doSomething這個(gè)函數(shù),然后打印出了:這里是某車在使用簡單的JSONP
??流程圖
- 全局聲明一個(gè)回調(diào)函數(shù)
- 在<script>標(biāo)簽中,輸入U(xiǎn)RL,向服務(wù)器傳遞該函數(shù)名和參數(shù)(可以通過問號(hào)傳參:?callback=doSomething)
- 服務(wù)器接收到請求后,需要進(jìn)行處理:把傳遞進(jìn)來的函數(shù)名和要返回的數(shù)據(jù)拼接為函數(shù)調(diào)用式,例如:傳遞進(jìn)去的函數(shù)名是doSomething,要傳回的數(shù)據(jù)是:這里是某車在使用簡單的JSONP,那么后臺(tái)返回的即為doSomething('這里是某車在使用簡單的JSONP')
- 客戶端再調(diào)用執(zhí)行之前聲明的回調(diào)函數(shù)(doSomething),對返回的數(shù)據(jù)進(jìn)行操作
??封裝
但是在真正的開發(fā)中,我們不可能寫一堆<script>標(biāo)簽在那里,所以我們需要對jsonp進(jìn)行封裝
function jsonp({url, params, callback}){ return new Promise((resolve, reject) => { //動(dòng)態(tài)創(chuàng)建script標(biāo)簽 let script = document.createElement('script'); //處理傳入的參數(shù) params = {...params, callback}; //轉(zhuǎn)換參數(shù)表達(dá)式 let arr = [] for(let key in params) { arr.push(`${key}=${params[key]}`) } //在路徑中,參數(shù)用 & 隔開 script.src = `${url}?${arr.join('&')}` //添加 script 標(biāo)簽 document.body.appendChild(script); //聲明回調(diào)函數(shù) window[callback] = function(data) { //執(zhí)行異步函數(shù) resolve(data); //請求完后移除該script標(biāo)簽 document.body.removeChild(script) } }) }
- 使用
jsonp({ url:'http://localhost:3000/test', params:{ args:'這個(gè)是參數(shù)' }, callback: 'doSomething' }).then(res=>{ console.log(res) })
??優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn): 簡單兼容性好,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題
- 缺點(diǎn): 僅支持get方法具有局限性,不安全可能會(huì)遭受XSS攻擊
CORS(Cross-Origin Resource Sharing)
- 實(shí)現(xiàn)CORS的關(guān)鍵在于后端:服務(wù)端設(shè)置Access-Control-Allow-Origin就可以開啟 CORS。 該屬性表示哪些域名可以訪問資源,如果設(shè)置通配符則表示所有網(wǎng)站都可以訪問資源
- 我們可以將請求分類為簡單請求和非簡單請求
簡單請求
判定:
請求方法是以下方三種方法之一
- HEAD
- GET
- POST
請求頭僅包含安全的字段,如以下幾種字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三個(gè)值 application/x-www-form-urlencoded、multipart/form-data、text/plain
請求過程
- 客戶端代碼
btn.addEventListener('click',()=>{ const xhr = new XMLHttpRequest(); xhr.open('get', 'http://localhost:3000/test'); //向端口號(hào)為3000的發(fā)起請求 xhr.send(); xhr.onreadystatechange = function(){ if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status < 300) { console.log(xhr.response) //如果請求成功,則打印后臺(tái)返回的數(shù)據(jù) } } } })
- 服務(wù)端設(shè)置
app.get('/test',(req,res)=>{ //通過res.send()方法-將處理好的內(nèi)容,發(fā)送給客戶 res.send(`你好呀某車,你已經(jīng)成功發(fā)起請求了`) })
- 當(dāng)你發(fā)送請求的時(shí)候(客戶端為5500端口,服務(wù)端為3000端口),什么也不設(shè)置就會(huì)報(bào)跨域錯(cuò)誤
- 此時(shí)我們看一下當(dāng)瀏覽器會(huì)給它的請求頭加上Origin,值為它本身的協(xié)議+域名+端口,這個(gè)時(shí)候服務(wù)器就能知道是誰在對他發(fā)送請求
- 那么服務(wù)器如果想要允許它訪問的話,則在響應(yīng)頭加上Access-Control-Allow-Origin字段,值可以為*表示允許所有源的訪問,也可以為具體的源,我們這里給他設(shè)置為我們上述的Origin值,同時(shí)也加上Access-Control-Allow-Methods字段,設(shè)置可訪問的請求方法
app.all("*",function(req,res,next){ //設(shè)置允許跨域的域名,*代表允許任意域名跨域 res.header("Access-Control-Allow-Origin","http://127.0.0.1:5500"); res.header("Access-Control-Allow-Methods",'PUT,POST,GET,DELETE,OPTIONS') next(); })
- 此時(shí)你再點(diǎn)擊發(fā)送請求就會(huì)發(fā)現(xiàn):請求已經(jīng)能夠正常返回了
- 響應(yīng)頭顯示的Access-Control-Allow-Origin即為我們可訪問的源
復(fù)雜請求
- 我們上述已經(jīng)講過什么是簡單請求了,那么不符合上述條件的即為復(fù)雜請求
- 復(fù)雜請求發(fā)起請求的時(shí)候則是會(huì)在正式通信之前進(jìn)行一次預(yù)檢請求(preflight request)
- 瀏覽器先詢問服務(wù)器,當(dāng)前源當(dāng)前請求是否可以訪問服務(wù)器資源,只有得到正確的答復(fù),才會(huì)進(jìn)行正式的請求
請求過程
我們依舊使用上面的代碼,然后就請求方式改為put,此時(shí)發(fā)送的即為復(fù)雜請求,可以看到發(fā)送了兩次請求,由于我們put方法在上述已經(jīng)被設(shè)置為可訪問的方法,所以現(xiàn)在預(yù)檢可以通過,我們也能正常請求到資源
如果我們將put方法從Access-Cntrol-Allow-Methods移除,此時(shí)再進(jìn)行請求,則發(fā)提示,在預(yù)檢請求中發(fā)送PUT方法是不被允許的
此時(shí)test正式請求則報(bào)CORS錯(cuò)誤
寫到這里,我就想到了曾經(jīng)看過的面試題:跨域請求如何攜帶cookie
擴(kuò)展
- 但是我想測試的時(shí)候受到了SameSite的限制,不允許第三方cookie,本來想要關(guān)掉瀏覽器設(shè)置的,搜索了一下發(fā)現(xiàn)這個(gè)方法自從版本91之后就用不了,那么淺淺說一下方法就得了
- 只需要前端在發(fā)送Ajax請求的時(shí)候?qū)ithCredentials設(shè)置為true
const xhr = new XMLHttpRequest(); xhr.withCredentials = true;
- 后端設(shè)置"Access-Control-Allow-Credentials為true,同時(shí)注意Access-Control-Allow-Origin值不為*
res.header("Access-Control-Allow-Credentials", "true");
缺點(diǎn)
IE10以下不支持
小結(jié):后端是關(guān)鍵,主要是設(shè)置Access-Control-Allow-Origin
關(guān)于postMessage和webSocket的跨域方案我寫在嗦嗦postMessage和webSocket這里了
以上就是簡單易懂的JSONP和CORS跨域方案詳解的詳細(xì)內(nèi)容,更多關(guān)于JSONP CORS跨域方案的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js 判斷當(dāng)前時(shí)間是否處于某個(gè)一個(gè)時(shí)間段內(nèi)
這篇文章主要介紹了js 判斷當(dāng)前時(shí)間是否處于某個(gè)一個(gè)時(shí)間段內(nèi),使用 jutils - JavaScript常用函數(shù)庫的 isDuringDate 函數(shù)來實(shí)現(xiàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09JavaScript使用showdown實(shí)現(xiàn)markdown預(yù)覽功能
Showdown.js 是一個(gè)JavaScript庫,用于將Markdown文本轉(zhuǎn)換為HTML,這篇文章將給大家介紹了JavaScript使用showdown實(shí)現(xiàn)markdown預(yù)覽功能,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01利用Query+bootstrap和js兩種方式實(shí)現(xiàn)日期選擇器
日期選擇器在我們平時(shí)開發(fā)的時(shí)候經(jīng)常要用到,下面這篇文章主要給大家介紹了利用Query+bootstrap和js這兩種方式實(shí)現(xiàn)日期選擇器的方法,文中兩種方法都給出了詳細(xì)的示例代碼,有需要的朋友可以參考借鑒,下面來一起看看吧。2017-01-01Javascript通過overflow控制列表閉合與展開的方法
這篇文章主要介紹了Javascript通過overflow控制列表閉合與展開的方法,設(shè)計(jì)javascript動(dòng)態(tài)操作頁面元素與樣式的相關(guān)技巧,需要的朋友可以參考下2015-05-05JScript中的undefined和"undefined"的區(qū)別
JScript中的undefined和"undefined"的區(qū)別...2007-03-03