<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          看完這篇,我再也不問(wèn)跨域的問(wèn)題了

          共 9044字,需瀏覽 19分鐘

           ·

          2020-11-17 11:13

          跨域這兩個(gè)字就像一塊狗皮膏藥一樣黏在每一個(gè)前端開(kāi)發(fā)者身上,無(wú)論你在工作上或者面試中無(wú)可避免會(huì)遇到這個(gè)問(wèn)題。為了應(yīng)付面試,我每次都隨便背幾個(gè)方案,也不知道為什么要這樣干,反正面完就可以扔了,我想工作上也不會(huì)用到那么多亂七八糟的方案。到了真正工作,開(kāi)發(fā)環(huán)境有webpack-dev-server搞定,上線了服務(wù)端的大佬們也會(huì)配好,配了什么我不管,反正不會(huì)跨域就是了。日子也就這么混過(guò)去了,終于有一天,我覺(jué)得不能再繼續(xù)這樣混下去了,我一定要徹底搞懂這個(gè)東西!于是就有了這篇文章。


          要掌握跨域,首先要知道為什么會(huì)有跨域這個(gè)問(wèn)題出現(xiàn)

          確實(shí),我們這種搬磚工人就是為了混口飯吃嘛,好好的調(diào)個(gè)接口告訴我跨域了,這種阻礙我們輕松搬磚的事情真惡心!為什么會(huì)跨域?是誰(shuí)在搞事情?為了找到這個(gè)問(wèn)題的始作俑者,請(qǐng)點(diǎn)擊:瀏覽器的同源策略。

          這么官方的東西真難懂,沒(méi)關(guān)系,至少你知道了,因?yàn)闉g覽器的同源策略導(dǎo)致了跨域,就是瀏覽器在搞事情。

          所以,瀏覽器為什么要搞事情?就是不想給好日子我們過(guò)?對(duì)于這樣的質(zhì)問(wèn),瀏覽器甩鍋道:“同源策略限制了從同一個(gè)源加載的文檔或腳本如何與來(lái)自另一個(gè)源的資源進(jìn)行交互。這是一個(gè)用于隔離潛在惡意文件的重要安全機(jī)制?!?/p>

          這么官方的話術(shù)真難懂,沒(méi)關(guān)系,至少你知道了,似乎這是個(gè)安全機(jī)制。

          所以,究竟為什么需要這樣的安全機(jī)制?這樣的安全機(jī)制解決了什么問(wèn)題?別急,讓我們繼續(xù)研究下去。


          沒(méi)有同源策略限制的兩大危險(xiǎn)場(chǎng)景

          據(jù)我了解,瀏覽器是從兩個(gè)方面去做這個(gè)同源策略的,一是針對(duì)接口的請(qǐng)求,二是針對(duì)Dom的查詢。試想一下沒(méi)有這樣的限制上述兩種動(dòng)作有什么危險(xiǎn)。

          沒(méi)有同源策略限制的接口請(qǐng)求

          有一個(gè)小小的東西叫cookie大家應(yīng)該知道,一般用來(lái)處理登錄等場(chǎng)景,目的是讓服務(wù)端知道誰(shuí)發(fā)出的這次請(qǐng)求。如果你請(qǐng)求了接口進(jìn)行登錄,服務(wù)端驗(yàn)證通過(guò)后會(huì)在響應(yīng)頭加入Set-Cookie字段,然后下次再發(fā)請(qǐng)求的時(shí)候,瀏覽器會(huì)自動(dòng)將cookie附加在HTTP請(qǐng)求的頭字段Cookie中,服務(wù)端就能知道這個(gè)用戶已經(jīng)登錄過(guò)了。知道這個(gè)之后,我們來(lái)看場(chǎng)景:

          1. 你準(zhǔn)備去清空你的購(gòu)物車,于是打開(kāi)了買買買網(wǎng)站www.maimaimai.com,然后登錄成功,一看,購(gòu)物車東西這么少,不行,還得買多點(diǎn)。

          2. 你在看有什么東西買的過(guò)程中,你的好基友發(fā)給你一個(gè)鏈接www.nidongde.com,一臉yin笑地跟你說(shuō):“你懂的”,你毫不猶豫打開(kāi)了。

          3. 你饒有興致地瀏覽著www.nidongde.com,誰(shuí)知這個(gè)網(wǎng)站暗地里做了些不可描述的事情!由于沒(méi)有同源策略的限制,它向www.maimaimai.com發(fā)起了請(qǐng)求!聰明的你一定想到上面的話“服務(wù)端驗(yàn)證通過(guò)后會(huì)在響應(yīng)頭加入Set-Cookie字段,然后下次再發(fā)請(qǐng)求的時(shí)候,瀏覽器會(huì)自動(dòng)將cookie附加在HTTP請(qǐng)求的頭字段Cookie中”,這樣一來(lái),這個(gè)不法網(wǎng)站就相當(dāng)于登錄了你的賬號(hào),可以為所欲為了!如果這不是一個(gè)買買買賬號(hào),而是你的銀行賬號(hào),那……

          這就是傳說(shuō)中的CSRF攻擊(淺談CSRF攻擊方式)。

          看了這波CSRF攻擊我在想,即使有了同源策略限制,但cookie是明文的,還不是一樣能拿下來(lái)。于是我看了一些cookie相關(guān)的文章:聊一聊 cookie、Cookie/Session的機(jī)制與安全,知道了服務(wù)端可以設(shè)置httpOnly,使得前端無(wú)法操作cookie,如果沒(méi)有這樣的設(shè)置,像XSS攻擊就可以去獲取到cookie(Web安全測(cè)試之XSS);設(shè)置secure,則保證在https的加密通信中傳輸以防截獲。

          沒(méi)有同源策略限制的Dom查詢
          1. 有一天你剛睡醒,收到一封郵件,說(shuō)是你的銀行賬號(hào)有風(fēng)險(xiǎn),趕緊點(diǎn)進(jìn)www.yinghang.com改密碼。你嚇尿了,趕緊點(diǎn)進(jìn)去,還是熟悉的銀行登錄界面,你果斷輸入你的賬號(hào)密碼,登錄進(jìn)去看看錢有沒(méi)有少了。

          2. 睡眼朦朧的你沒(méi)看清楚,平時(shí)訪問(wèn)的銀行網(wǎng)站是www.yinhang.com,而現(xiàn)在訪問(wèn)的是www.yinghang.com,這個(gè)釣魚(yú)網(wǎng)站做了什么呢?

          1. // HTML

          2. <iframe name="yinhang" src="www.yinhang.com">iframe>

          3. // JS

          4. // 由于沒(méi)有同源策略的限制,釣魚(yú)網(wǎng)站可以直接拿到別的網(wǎng)站的Dom

          5. const iframe = window.frames['yinhang']

          6. const node = iframe.document.getElementById('你輸入賬號(hào)密碼的Input')

          7. console.log(`拿到了這個(gè)${node},我還拿不到你剛剛輸入的賬號(hào)密碼嗎`)

          1. ? ?

          由此我們知道,同源策略確實(shí)能規(guī)避一些危險(xiǎn),不是說(shuō)有了同源策略就安全,只是說(shuō)同源策略是一種瀏覽器最基本的安全機(jī)制,畢竟能提高一點(diǎn)攻擊的成本。其實(shí)沒(méi)有刺不穿的盾,只是攻擊的成本和攻擊成功后獲得的利益成不成正比。

          跨域正確的打開(kāi)方式

          經(jīng)過(guò)對(duì)同源策略的了解,我們應(yīng)該要消除對(duì)瀏覽器的誤解,同源策略是瀏覽器做的一件好事,是用來(lái)防御來(lái)自邪門歪道的攻擊,但總不能為了不讓壞人進(jìn)門而把全部人都拒之門外吧。沒(méi)錯(cuò),我們這種正人君子只要打開(kāi)方式正確,就應(yīng)該可以跨域。

          下面將一個(gè)個(gè)演示正確打開(kāi)方式,但在此之前,有些準(zhǔn)備工作要做。為了本地演示跨域,我們需要:

          1. 隨便跑起一份前端代碼(以下前端是隨便跑起來(lái)的vue),地址是http://localhost:9099。

          2. 隨便跑起一份后端代碼(以下后端是隨便跑起來(lái)的node koa2),地址是http://localhost:9971。

          同源策略限制下接口請(qǐng)求的正確打開(kāi)方式

          1.JSONP

          在HTML標(biāo)簽里,一些標(biāo)簽比如script、img這樣的獲取資源的標(biāo)簽是沒(méi)有跨域限制的,利用這一點(diǎn),我們可以這樣干。

          后端寫(xiě)個(gè)小接口:

          1. // 處理成功失敗返回格式的工具

          2. const{successBody}=require('../utli')

          3. classCrossDomain{

          4. ?staticasync jsonp (ctx){

          5. ? ?// 前端傳過(guò)來(lái)的參數(shù)

          6. ? ?const query = ctx.request.query

          7. ? ?// 設(shè)置一個(gè)cookies

          8. ? ?ctx.cookies.set('tokenId','1')

          9. ? ?// query.cb是前后端約定的方法名字,其實(shí)就是后端返回一個(gè)直接執(zhí)行的方法給前端,由于前端是用script標(biāo)簽發(fā)起的請(qǐng)求,所以返回了這個(gè)方法后相當(dāng)于立馬執(zhí)行,并且把要返回的數(shù)據(jù)放在方法的參數(shù)里。

          10. ? ?ctx.body =`${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`

          11. ?}

          12. }

          13. module.exports =CrossDomain

          簡(jiǎn)單版前端:

          1. ?

          2. ? ?charset="utf-8">

          3. ?

          4. ?

          5. ? ?

          6. ? ?

          7. ?

          簡(jiǎn)單封裝一下前端這個(gè)套路:

          1. /**

          2. * JSONP請(qǐng)求工具

          3. * @param url 請(qǐng)求的地址

          4. * @param data 請(qǐng)求的參數(shù)

          5. * @returns {Promise}

          6. */

          7. const request =({url, data})=>{

          8. ?returnnewPromise((resolve, reject)=>{

          9. ? ?// 處理傳參成xx=yy&aa=bb的形式

          10. ? ?const handleData =(data)=>{

          11. ? ? ?const keys =Object.keys(data)

          12. ? ? ?const keysLen = keys.length

          13. ? ? ?return keys.reduce((pre, cur, index)=>{

          14. ? ? ? ?const value = data[cur]

          15. ? ? ? ?const flag = index !== keysLen -1?'&':''

          16. ? ? ? ?return`${pre}${cur}=${value}${flag}`

          17. ? ? ?},'')

          18. ? ?}

          19. ? ?// 動(dòng)態(tài)創(chuàng)建script標(biāo)簽

          20. ? ?const script = document.createElement('script')

          21. ? ?// 接口返回的數(shù)據(jù)獲取

          22. ? ?window.jsonpCb =(res)=>{

          23. ? ? ?document.body.removeChild(script)

          24. ? ? ?delete window.jsonpCb

          25. ? ? ?resolve(res)

          26. ? ?}

          27. ? ?script.src =`${url}?${handleData(data)}&cb=jsonpCb`

          28. ? ?document.body.appendChild(script)

          29. ?})

          30. }

          31. // 使用方式

          32. request({

          33. ?url:'http://localhost:9871/api/jsonp',

          34. ?data:{

          35. ? ?// 傳參

          36. ? ?msg:'helloJsonp'

          37. ?}

          38. }).then(res =>{

          39. ?console.log(res)

          40. })

          2.空iframe加form

          細(xì)心的朋友可能發(fā)現(xiàn),JSONP只能發(fā)GET請(qǐng)求,因?yàn)楸举|(zhì)上script加載資源就是GET,那么如果要發(fā)POST請(qǐng)求怎么辦呢?

          后端寫(xiě)個(gè)小接口:

          1. // 處理成功失敗返回格式的工具

          2. const{successBody}=require('../utli')

          3. classCrossDomain{

          4. ?staticasync iframePost (ctx){

          5. ? ?let postData = ctx.request.body

          6. ? ?console.log(postData)

          7. ? ?ctx.body = successBody({postData: postData},'success')

          8. ?}

          9. }

          10. module.exports =CrossDomain

          前端:

          1. const requestPost =({url, data})=>{

          2. ?// 首先創(chuàng)建一個(gè)用來(lái)發(fā)送數(shù)據(jù)的iframe.

          3. ?const iframe = document.createElement('iframe')

          4. ?iframe.name ='iframePost'

          5. ?iframe.style.display ='none'

          6. ?document.body.appendChild(iframe)

          7. ?const form = document.createElement('form')

          8. ?const node = document.createElement('input')

          9. ?// 注冊(cè)iframe的load事件處理程序,如果你需要在響應(yīng)返回時(shí)執(zhí)行一些操作的話.

          10. ?iframe.addEventListener('load',function(){

          11. ? ?console.log('post success')

          12. ?})


          13. ?form.action = url

          14. ?// 在指定的iframe中執(zhí)行form

          15. ?form.target = iframe.name

          16. ?form.method ='post'

          17. ?for(let name in data){

          18. ? ?node.name = name

          19. ? ?node.value = data[name].toString()

          20. ? ?form.appendChild(node.cloneNode())

          21. ?}

          22. ?// 表單元素需要添加到主文檔中.

          23. ?form.style.display ='none'

          24. ?document.body.appendChild(form)

          25. ?form.submit()


          26. ?// 表單提交后,就可以刪除這個(gè)表單,不影響下次的數(shù)據(jù)發(fā)送.

          27. ?document.body.removeChild(form)

          28. }

          29. // 使用方式

          30. requestPost({

          31. ?url:'http://localhost:9871/api/iframePost',

          32. ?data:{

          33. ? ?msg:'helloIframePost'

          34. ?}

          35. })

          3.CORS

          CORS是一個(gè)W3C標(biāo)準(zhǔn),全稱是"跨域資源共享"(Cross-origin resource sharing)跨域資源共享 CORS 詳解??疵志椭肋@是處理跨域問(wèn)題的標(biāo)準(zhǔn)做法。CORS有兩種請(qǐng)求,簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求。

          這里引用上面鏈接阮一峰老師的文章說(shuō)明一下簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求。

          瀏覽器將CORS請(qǐng)求分成兩類:簡(jiǎn)單請(qǐng)求(simple request)和非簡(jiǎn)單請(qǐng)求(not-so-simple request)。

          只要同時(shí)滿足以下兩大條件,就屬于簡(jiǎn)單請(qǐng)求。

          (1)請(qǐng)求方法是以下三種方法之一:

          • HEAD

          • GET

          • POST

          (2)HTTP的頭信息不超出以下幾種字段:

          • Accept

          • Accept-Language

          • Content-Language

          • Last-Event-ID

          • Content-Type:只限于三個(gè)值application/x-www-form-urlencoded、multipart/form-data、text/plain

          1、簡(jiǎn)單請(qǐng)求

          后端:

          1. // 處理成功失敗返回格式的工具

          2. const{successBody}=require('../utli')

          3. classCrossDomain{

          4. ?staticasync cors (ctx){

          5. ? ?const query = ctx.request.query

          6. ? ?// *時(shí)cookie不會(huì)在http請(qǐng)求中帶上

          7. ? ?ctx.set('Access-Control-Allow-Origin','*')

          8. ? ?ctx.cookies.set('tokenId','2')

          9. ? ?ctx.body = successBody({msg: query.msg},'success')

          10. ?}

          11. }

          12. module.exports =CrossDomain

          前端什么也不用干,就是正常發(fā)請(qǐng)求就可以,如果需要帶cookie的話,前后端都要設(shè)置一下,下面那個(gè)非簡(jiǎn)單請(qǐng)求例子會(huì)看到。

          1. fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res =>{

          2. ?console.log(res)

          3. })

          2、非簡(jiǎn)單請(qǐng)求 非簡(jiǎn)單請(qǐng)求會(huì)發(fā)出一次預(yù)檢測(cè)請(qǐng)求,返回碼是204,預(yù)檢測(cè)通過(guò)才會(huì)真正發(fā)出請(qǐng)求,這才返回200。這里通過(guò)前端發(fā)請(qǐng)求的時(shí)候增加一個(gè)額外的headers來(lái)觸發(fā)非簡(jiǎn)單請(qǐng)求。

          b333f7cb7e1cb9c2fc52c5f8d4946c86.webp

          后端:

          1. // 處理成功失敗返回格式的工具

          2. const{successBody}=require('../utli')

          3. classCrossDomain{

          4. ?staticasync cors (ctx){

          5. ? ?const query = ctx.request.query

          6. ? ?// 如果需要http請(qǐng)求中帶上cookie,需要前后端都設(shè)置credentials,且后端設(shè)置指定的origin

          7. ? ?ctx.set('Access-Control-Allow-Origin','http://localhost:9099')

          8. ? ?ctx.set('Access-Control-Allow-Credentials',true)

          9. ? ?// 非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求,會(huì)在正式通信之前,增加一次HTTP查詢請(qǐng)求,稱為"預(yù)檢"請(qǐng)求(preflight)

          10. ? ?// 這種情況下除了設(shè)置origin,還需要設(shè)置Access-Control-Request-Method以及Access-Control-Request-Headers

          11. ? ?ctx.set('Access-Control-Request-Method','PUT,POST,GET,DELETE,OPTIONS')

          12. ? ?ctx.set('Access-Control-Allow-Headers','Origin, X-Requested-With, Content-Type, Accept, t')

          13. ? ?ctx.cookies.set('tokenId','2')


          14. ? ?ctx.body = successBody({msg: query.msg},'success')

          15. ?}

          16. }

          17. module.exports =CrossDomain

          一個(gè)接口就要寫(xiě)這么多代碼,如果想所有接口都統(tǒng)一處理,有什么更優(yōu)雅的方式呢?見(jiàn)下面的koa2-cors。

          1. const path =require('path')

          2. constKoa=require('koa')

          3. const koaStatic =require('koa-static')

          4. const bodyParser =require('koa-bodyparser')

          5. const router =require('./router')

          6. const cors =require('koa2-cors')

          7. const app =newKoa()

          8. const port =9871

          9. app.use(bodyParser())

          10. // 處理靜態(tài)資源 這里是前端build好之后的目錄

          11. app.use(koaStatic(

          12. ?path.resolve(__dirname,'../dist')

          13. ))

          14. // 處理cors

          15. app.use(cors({

          16. ?origin:function(ctx){

          17. ? ?return'http://localhost:9099'

          18. ?},

          19. ?credentials:true,

          20. ?allowMethods:['GET','POST','DELETE'],

          21. ?allowHeaders:['t','Content-Type']

          22. }))

          23. // 路由

          24. app.use(router.routes()).use(router.allowedMethods())

          25. // 監(jiān)聽(tīng)端口

          26. app.listen(9871)

          27. console.log(`[demo] start-quick is starting at port ${port}`)

          前端:

          1. fetch(`http://localhost:9871/api/cors?msg=helloCors`,{

          2. ?// 需要帶上cookie

          3. ?credentials:'include',

          4. ?// 這里添加額外的headers來(lái)觸發(fā)非簡(jiǎn)單請(qǐng)求

          5. ?headers:{

          6. ? ?'t':'extra headers'

          7. ?}

          8. }).then(res =>{

          9. ?console.log(res)

          10. })

          4.代理

          想一下,如果我們請(qǐng)求的時(shí)候還是用前端的域名,然后有個(gè)東西幫我們把這個(gè)請(qǐng)求轉(zhuǎn)發(fā)到真正的后端域名上,不就避免跨域了嗎?這時(shí)候,Nginx出場(chǎng)了。

          Nginx配置:

          1. server{

          2. ? ?# 監(jiān)聽(tīng)9099端口

          3. ? ?listen 9099;

          4. ? ?# 域名是localhost

          5. ? ?server_name localhost;

          6. ? ?#凡是localhost:9099/api這個(gè)樣子的,都轉(zhuǎn)發(fā)到真正的服務(wù)端地址http://localhost:9871

          7. ? ?location ^~/api {

          8. ? ? ? ?proxy_pass http://localhost:9871;

          9. ? ?} ?

          10. }

          前端就不用干什么事情了,除了寫(xiě)接口,也沒(méi)后端什么事情了。

          1. // 請(qǐng)求的時(shí)候直接用回前端這邊的域名http://localhost:9099,這就不會(huì)跨域,然后Nginx監(jiān)聽(tīng)到凡是localhost:9099/api這個(gè)樣子的,都轉(zhuǎn)發(fā)到真正的服務(wù)端地址http://localhost:9871

          2. fetch('http://localhost:9099/api/iframePost',{

          3. ?method:'POST',

          4. ?headers:{

          5. ? ?'Accept':'application/json',

          6. ? ?'Content-Type':'application/json'

          7. ?},

          8. ?body: JSON.stringify({

          9. ? ?msg:'helloIframePost'

          10. ?})

          11. })

          Nginx轉(zhuǎn)發(fā)的方式似乎很方便!但這種使用也是看場(chǎng)景的,如果后端接口是一個(gè)公共的API,比如一些公共服務(wù)獲取天氣什么的,前端調(diào)用的時(shí)候總不能讓運(yùn)維去配置一下Nginx,如果兼容性沒(méi)問(wèn)題(IE 10或者以上),CROS才是更通用的做法吧。

          同源策略限制下Dom查詢的正確打開(kāi)方式

          1.postMessage

          window.postMessage() 是HTML5的一個(gè)接口,專注實(shí)現(xiàn)不同窗口不同頁(yè)面的跨域通訊。

          為了演示方便,我們將hosts改一下:127.0.0.1 crossDomain.com,現(xiàn)在訪問(wèn)域名crossDomain.com就等于訪問(wèn)127.0.0.1。

          這里是http://localhost:9099/#/crossDomain,發(fā)消息方:


          這里是http://crossdomain.com:9099,接收消息方:


          結(jié)果可以看到:

          daea8afeb31fc565ef785ebe516b2252.webp

          2.document.domain

          這種方式只適合主域名相同,但子域名不同的iframe跨域。

          比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,這種情況下給兩個(gè)頁(yè)面指定一下document.domain即document.domain = crossdomain.com就可以訪問(wèn)各自的window對(duì)象了。

          3.canvas操作圖片的跨域問(wèn)題

          這個(gè)應(yīng)該是一個(gè)比較冷門的跨域問(wèn)題,張大神已經(jīng)寫(xiě)過(guò)了我就不再班門弄斧了:解決canvas圖片getImageData,toDataURL跨域問(wèn)題


          最后

          希望看完這篇文章之后,再有人問(wèn)跨域的問(wèn)題,你可以嘴角微微上揚(yáng),冷笑一聲,“不要再問(wèn)我跨域的問(wèn)題了”,揚(yáng)長(zhǎng)而去。


          源自:https://segmentfault.com/a/1190000015597029

          聲明:文章著作權(quán)歸作者所有,如有侵權(quán),請(qǐng)聯(lián)系小編刪除。




          瀏覽 119
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看国产一区 | 18禁成人黄官网 | 女人久久| 九色国产在线 | 色伊人大香蕉 |