不要再問我跨域的問題了

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。

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

要掌握跨域,首先要知道為什么會有跨域這個問題出現

確實,我們這種搬磚工人就是為了混口飯吃嘛,好好的調個接口告訴我跨域了,這種阻礙我們輕松搬磚的事情真惡心!為什么會跨域?是誰在搞事情?為了找到這個問題的始作俑者,請點擊瀏覽器的同源策略。
這么官方的東西真難懂,沒關系,至少你知道了,因為瀏覽器的同源策略導致了跨域,就是瀏覽器在搞事情。
所以,瀏覽器為什么要搞事情?就是不想給好日子我們過?對于這樣的質問,瀏覽器甩鍋道:“同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。”
這么官方的話術真難懂,沒關系,至少你知道了,似乎這是個安全機制。
所以,究竟為什么需要這樣的安全機制?這樣的安全機制解決了什么問題?別急,讓我們繼續研究下去。

沒有同源策略限制的兩大危險場景

據我了解,瀏覽器是從兩個方面去做這個同源策略的,一是針對接口的請求,二是針對Dom的查詢。試想一下沒有這樣的限制上述兩種動作有什么危險。

沒有同源策略限制的接口請求

有一個小小的東西叫cookie大家應該知道,一般用來處理登錄等場景,目的是讓服務端知道誰發出的這次請求。如果你請求了接口進行登錄,服務端驗證通過后會在響應頭加入Set-Cookie字段,然后下次再發請求的時候,瀏覽器會自動將cookie附加在HTTP請求的頭字段Cookie中,服務端就能知道這個用戶已經登錄過了。知道這個之后,我們來看場景:
1.你準備去清空你的購物車,于是打開了買買買網站www.maimaimai.com,然后登錄成功,一看,購物車東西這么少,不行,還得買多點。
2.你在看有什么東西買的過程中,你的好基友發給你一個鏈接www.nidongde.com,一臉yin笑地跟你說:“你懂的”,你毫不猶豫打開了。
3.你饒有興致地瀏覽著www.nidongde.com,誰知這個網站暗地里做了些不可描述的事情!由于沒有同源策略的限制,它向www.maimaimai.com發起了請求!聰明的你一定想到上面的話“服務端驗證通過后會在響應頭加入Set-Cookie字段,然后下次再發請求的時候,瀏覽器會自動將cookie附加在HTTP請求的頭字段Cookie中”,這樣一來,這個不法網站就相當于登錄了你的賬號,可以為所欲為了!如果這不是一個買買買賬號,而是你的銀行賬號,那……
這就是傳說中的CSRF攻擊淺談CSRF攻擊方式。
看了這波CSRF攻擊我在想,即使有了同源策略限制,但cookie是明文的,還不是一樣能拿下來。于是我看了一些cookie相關的文章聊一聊 cookie、Cookie/Session的機制與安全,知道了服務端可以設置httpOnly,使得前端無法操作cookie,如果沒有這樣的設置,像XSS攻擊就可以去獲取到cookieWeb安全測試之XSS;設置secure,則保證在https的加密通信中傳輸以防截獲。

沒有同源策略限制的Dom查詢

1.有一天你剛睡醒,收到一封郵件,說是你的銀行賬號有風險,趕緊點進www.yinghang.com改密碼。你嚇尿了,趕緊點進去,還是熟悉的銀行登錄界面,你果斷輸入你的賬號密碼,登錄進去看看錢有沒有少了。
2.睡眼朦朧的你沒看清楚,平時訪問的銀行網站是www.yinhang.com,而現在訪問的是www.yinghang.com,這個釣魚網站做了什么呢?

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于沒有同源策略的限制,釣魚網站可以直接拿到別的網站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你輸入賬號密碼的Input')
console.log(`拿到了這個${node},我還拿不到你剛剛輸入的賬號密碼嗎`)

由此我們知道,同源策略確實能規避一些危險,不是說有了同源策略就安全,只是說同源策略是一種瀏覽器最基本的安全機制,畢竟能提高一點攻擊的成本。其實沒有刺不穿的盾,只是攻擊的成本和攻擊成功后獲得的利益成不成正比。

跨域正確的打開方式

經過對同源策略的了解,我們應該要消除對瀏覽器的誤解,同源策略是瀏覽器做的一件好事,是用來防御來自邪門歪道的攻擊,但總不能為了不讓壞人進門而把全部人都拒之門外吧。沒錯,我們這種正人君子只要打開方式正確,就應該可以跨域。
下面將一個個演示正確打開方式,但在此之前,有些準備工作要做。為了本地演示跨域,我們需要:
1.隨便跑起一份前端代碼(以下前端是隨便跑起來的vue),地址是http://localhost:9099。
2.隨便跑起一份后端代碼(以下后端是隨便跑起來的node koa2),地址是http://localhost:9971。

同源策略限制下接口請求的正確打開方式

1.JSONP
在HTML標簽里,一些標簽比如script、img這樣的獲取資源的標簽是沒有跨域限制的,利用這一點,我們可以這樣干:

后端寫個小接口

// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async jsonp (ctx) {// 前端傳過來的參數const query = ctx.request.query// 設置一個cookiesctx.cookies.set('tokenId', '1')// query.cb是前后端約定的方法名字,其實就是后端返回一個直接執行的方法給前端,由于前端是用script標簽發起的請求,所以返回了這個方法后相當于立馬執行,并且把要返回的數據放在方法的參數里。ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`}
}
module.exports = CrossDomain

簡單版前端

<!DOCTYPE html>
<html><head><meta charset="utf-8"></head><body><script type='text/javascript'>// 后端返回直接執行的方法,相當于執行這個方法,由于后端把返回的數據放在方法的參數里,所以這里能拿到res。window.jsonpCb = function (res) {console.log(res)}</script><script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script></body>
</html>

簡單封裝一下前端這個套路

/*** JSONP請求工具* @param url 請求的地址* @param data 請求的參數* @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.lengthreturn keys.reduce((pre, cur, index) => {const value = data[cur]const flag = index !== keysLen - 1 ? '&' : ''return `${pre}${cur}=${value}${flag}`}, '')}// 動態創建script標簽const script = document.createElement('script')// 接口返回的數據獲取window.jsonpCb = (res) => {document.body.removeChild(script)delete window.jsonpCbresolve(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)
})

2.空iframe加form
細心的朋友可能發現,JSONP只能發GET請求,因為本質上script加載資源就是GET,那么如果要發POST請求怎么辦呢?

后端寫個小接口

// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async iframePost (ctx) {let postData = ctx.request.bodyconsole.log(postData)ctx.body = successBody({postData: postData}, 'success')}
}
module.exports = CrossDomain

前端

const requestPost = ({url, data}) => {// 首先創建一個用來發送數據的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')// 注冊iframe的load事件處理程序,如果你需要在響應返回時執行一些操作的話.iframe.addEventListener('load', function () {console.log('post success')})form.action = url// 在指定的iframe中執行formform.target = iframe.nameform.method = 'post'for (let name in data) {node.name = namenode.value = data[name].toString()form.appendChild(node.cloneNode())}// 表單元素需要添加到主文檔中.form.style.display = 'none'document.body.appendChild(form)form.submit()// 表單提交后,就可以刪除這個表單,不影響下次的數據發送.document.body.removeChild(form)
}
// 使用方式
requestPost({url: 'http://localhost:9871/api/iframePost',data: {msg: 'helloIframePost'}
})

3.CORS

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)跨域資源共享 CORS 詳解。看名字就知道這是處理跨域問題的標準做法。CORS有兩種請求,簡單請求和非簡單請求。

這里引用上面鏈接阮一峰老師的文章說明一下簡單請求和非簡單請求。
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

只要同時滿足以下兩大條件,就屬于簡單請求。
(1) 請求方法是以下三種方法之一:

  • HEAD
  • GET
  • POST

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

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

1.簡單請求
后端

// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async cors (ctx) {const query = ctx.request.query// *時cookie不會在http請求中帶上ctx.set('Access-Control-Allow-Origin', '*')ctx.cookies.set('tokenId', '2')ctx.body = successBody({msg: query.msg}, 'success')}
}
module.exports = CrossDomain

前端什么也不用干,就是正常發請求就可以,如果需要帶cookie的話,前后端都要設置一下,下面那個非簡單請求例子會看到。

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

2.非簡單請求
非簡單請求會發出一次預檢測請求,返回碼是204,預檢測通過才會真正發出請求,這才返回200。這里通過前端發請求的時候增加一個額外的headers來觸發非簡單請求。

后端

// 處理成功失敗返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {static async cors (ctx) {const query = ctx.request.query// 如果需要http請求中帶上cookie,需要前后端都設置credentials,且后端設置指定的originctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')ctx.set('Access-Control-Allow-Credentials', true)// 非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)// 這種情況下除了設置origin,還需要設置Access-Control-Request-Method以及Access-Control-Request-Headersctx.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

一個接口就要寫這么多代碼,如果想所有接口都統一處理,有什么更優雅的方式呢?見下面的koa2-cors。

const path = require('path')
const Koa = require('koa')
const koaStatic = require('koa-static')
const bodyParser = require('koa-bodyparser')
const router = require('./router')
const cors = require('koa2-cors')
const app = new Koa()
const port = 9871
app.use(bodyParser())
// 處理靜態資源 這里是前端build好之后的目錄
app.use(koaStatic(path.resolve(__dirname, '../dist')
))
// 處理cors
app.use(cors({origin: function (ctx) {return 'http://localhost:9099'},credentials: true,allowMethods: ['GET', 'POST', 'DELETE'],allowHeaders: ['t', 'Content-Type']
}))
// 路由
app.use(router.routes()).use(router.allowedMethods())
// 監聽端口
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)

前端

fetch(`http://localhost:9871/api/cors?msg=helloCors`, {// 需要帶上cookiecredentials: 'include',// 這里添加額外的headers來觸發非簡單請求headers: {'t': 'extra headers'}
}).then(res => {console.log(res)
})

4.代理
想一下,如果我們請求的時候還是用前端的域名,然后有個東西幫我們把這個請求轉發到真正的后端域名上,不就避免跨域了嗎?這時候,Nginx出場了。
Nginx配置

server{# 監聽9099端口listen 9099;# 域名是localhostserver_name localhost;#凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871 location ^~ /api {proxy_pass http://localhost:9871;}    
}

前端就不用干什么事情了,除了寫接口,也沒后端什么事情了

// 請求的時候直接用回前端這邊的域名http://localhost:9099,這就不會跨域,然后Nginx監聽到凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871 
fetch('http://localhost:9099/api/iframePost', {method: 'POST',headers: {'Accept': 'application/json','Content-Type': 'application/json'},body: JSON.stringify({msg: 'helloIframePost'})
})

Nginx轉發的方式似乎很方便!但這種使用也是看場景的,如果后端接口是一個公共的API,比如一些公共服務獲取天氣什么的,前端調用的時候總不能讓運維去配置一下Nginx,如果兼容性沒問題(IE 10或者以上),CROS才是更通用的做法吧。

同源策略限制下Dom查詢的正確打開方式

1.postMessage
window.postMessage() 是HTML5的一個接口,專注實現不同窗口不同頁面的跨域通訊。
為了演示方便,我們將hosts改一下:127.0.0.1 crossDomain.com,現在訪問域名crossDomain.com就等于訪問127.0.0.1。

這里是http://localhost:9099/#/crossDomain,發消息方

<template><div><button @click="postMessage">給http://crossDomain.com:9099發消息</button><iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe></div>
</template><script>
export default {mounted () {window.addEventListener('message', (e) => {// 這里一定要對來源做校驗if (e.origin === 'http://crossdomain.com:9099') {// 來自http://crossdomain.com:9099的結果回復console.log(e.data)}})},methods: {// 向http://crossdomain.com:9099發消息postMessage () {const iframe = window.frames['crossDomainIframe']iframe.postMessage('我是[http://localhost:9099], 麻煩你查一下你那邊有沒有id為app的Dom', 'http://crossdomain.com:9099')}}
}
</script>

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

<template><div>我是http://crossdomain.com:9099</div>
</template><script>
export default {mounted () {window.addEventListener('message', (e) => {// 這里一定要對來源做校驗if (e.origin === 'http://localhost:9099') {// http://localhost:9099發來的信息console.log(e.data)// e.source可以是回信的對象,其實就是http://localhost:9099窗口對象(window)的引用// e.origin可以作為targetOrigine.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,這就是你想知道的結果:${document.getElementById('app') ? '有id為app的Dom' : '沒有id為app的Dom'}`, e.origin);}})}
}
</script>

結果可以看到:

clipboard.png

2.document.domain
這種方式只適合主域名相同,但子域名不同的iframe跨域。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,這種情況下給兩個頁面指定一下document.domain即document.domain = crossdomain.com就可以訪問各自的window對象了。

3.canvas操作圖片的跨域問題
這個應該是一個比較冷門的跨域問題,張大神已經寫過了我就不再班門弄斧了解決canvas圖片getImageData,toDataURL跨域問題

最后

希望看完這篇文章之后,再有人問跨域的問題,你可以嘴角微微上揚,冷笑一聲:“不要再問我跨域的問題了。”
揚長而去。

?

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/448378.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/448378.shtml
英文地址,請注明出處:http://en.pswp.cn/news/448378.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SSM集合

SSM集成 1. Spring和各個框架的整合 Spring目前是JavaWeb開發中最終的框架&#xff0c;提供一站式服務&#xff0c;可以其他各個框架整合集成 Spring整合方案 1.1. SSH ssh是早期的一種整合方案 Struts2 &#xff1a; Web層框架 Spring &#xff1a; 容器框架 Hibernate &#…

淺談 CSRF 攻擊方式

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一.CSRF是什么&#xff1f; CSRF&#xff08;Cross-site request forgery&#xff09;&#xff0c;中文名稱&#xff1a;跨站請求偽造&a…

C++之運算符重載(上)

1、概念 所謂重載&#xff0c;就是重新賦予新的含義。函數重載就是對一個已有的函數賦予新的含義&#xff0c;使之實現新功能&#xff0c;因此&#xff0c;一個函數名就可以用來代表不同功能的函數&#xff0c;也就是”一名多用”。 運算符也可以重載。實際上&#xff0c;我們…

手剎

定義 考手剎的專業稱呼是輔助制動器&#xff0c;與制動器的原理不同&#xff0c;其是采用鋼絲拉線連接到后制動蹄上&#xff0c;以對車子進行制動。作用 用于平地斜坡停車時制動&#xff0c;防止車子在無人狀態下自動滑跑&#xff0c;逼免發生交通事故。工作原理 其原…

關于[super dealloc]

銷毀一個對象時&#xff0c;需要重寫系統的dealloc方法來釋放當前類所擁有的對象&#xff0c;在dealloc方法中需要先釋放當前類中所有的對象&#xff0c;然后再調用[super dealloc]釋放父類中所擁有的對象。如先調用[super dealloc]將釋放掉父類中所擁有的對象&#xff0c;當前…

C++之運算符重載(下)

4.提高 1.運算符重載機制 編譯器實現運算符重載實際上就是通過函數重載實現的&#xff0c;可分為全局函數方式&#xff0c;也可分為成員函數方式進行重載&#xff0c;并沒有改變原操作符的屬性和語義。只是針對某個特定類定義一種新的數據類型操作。 2.重載賦值運算符 賦值…

Cookie / Session 的機制與安全

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Cookie和Session是為了在無狀態的HTTP協議之上維護會話狀態&#xff0c;使得服務器可以知道當前是和哪個客戶在打交道。本文來詳細討論C…

手動擋

定義 手動擋&#xff0c;即用手撥動變速桿才能改變變速器內的齒輪嚙合置&#xff0c;改變傳動比&#xff0c;從而達到變速的目的。作用 一方面提供了手動的樂趣 另外一方面就是通過手動自主控制轉速&#xff0c;還可以遲延或提前換檔。駕駛技巧 市區內應直視前方五…

Servlet快速入門及運行流程

一、Servlet快速入門 1.創建一個web工程 2.在JavaResource中src下創建一個包名稱為com.myxq.servlet 3.在創建的servlet包當中創建一個class文件起名為FirstServlet 4.進入該class實現一個Servlet接口&#xff0c;實現它未實現的方法 重點看service方法在該方法當中寫入一句話進…

C++之多繼承

1.基礎知識 1.1 類之間的關系 has-A&#xff0c;uses-A 和 is-A has-A 包含關系&#xff0c;用以描述一個類由多個“部件類”構成。實現has-A關系用類成員表示&#xff0c;即一個類中的數據成員是另一種已經定義的類。 常和構造函數初始化列表一起使用 uses-A 一個類部分地…

自動擋

定義 所謂自動擋&#xff0c;就是不用駕駛者去手動換檔&#xff0c;車輛會根據行駛的速度和交通情況自動選擇合適的檔位行駛。作用 能根據路面狀況自動變速&#xff0c;使駕駛者可以全神貫地注視路面交通而不會被換檔搞得手忙腳亂。工作原理 自動變速器&#xff0c…

聊一聊 cookie

我們看到的 cookie 前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我自己創建了一個網站&#xff0c;網址為http://ppsc.sankuai.com。在這個網頁中我設置了幾個cookie&#xff1a;JS…

跨域資源共享 CORS 詳解

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 CORS是一個W3C標準&#xff0c;全稱是"跨域資源共享"&#xff08;Cross-origin resource sharing&#xff09;。 它允許瀏覽…

油門

定義 油門是內燃機上控制燃料供量的裝置。作用 是汽車發動機與摩托車油箱之間的閥門&#xff0c;控制汽油的量。操作注意 1.空車起步勿用大油門&#xff0c;以小油門為宜&#xff0c;負荷起步則以中油門為宜。 2.啟動時將油門放在合適位&#xff0c;使機件不易磨損。…

C++之泛型編程(模板)

1.模板綜述 背景 有時候許多函數或子程序的邏輯結構是一樣的&#xff0c;只是要處理的數據類型不一樣有時候多個類具有相同邏輯的成員函數和成員變量&#xff0c;只是成員變量的數據類型以及成員函數的參數類型不一樣模板就是解決數據類型不一致造成代碼冗余的一種機制&#xf…

Base64轉PDF、PDF轉IMG(使用pdfbox插件)

--添加依賴 <!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox --><dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.12</version></dependency&…

const的用法,特別是用在函數后面

原文出處&#xff1a;http://blog.csdn.net/zcf1002797280/article/details/7816977

圖解 Linux 安裝 JDK1.8 、配置環境變量

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. 到官網下載 JDK 1.8 https://www.oracle.com/technetwork/java/javase/downloads/index.html 2. 用 rz 命令把 jdk-8u191-linux-x6…

剎車

定義 剎車就是可以減慢車速的機械制動裝置&#xff0c;又名減速器。簡單來說&#xff0c;汽車剎車踏板在方向盤下面&#xff0c;踩住剎車踏板&#xff0c;則使剎車杠桿聯動受壓并傳至到剎車鼓上的剎車片卡住剎車輪盤&#xff0c;使汽車減速或停止運行。作用 目的是減速&a…

【原創】Performanced C++ 經驗規則 第五條:再談重載、覆蓋和隱藏

第五條&#xff1a;再談重載、覆蓋和隱藏 在C中&#xff0c;無論在類作用域內還是外&#xff0c;兩個&#xff08;或多個&#xff09;同名的函數&#xff0c;可能且僅可能是以下三種關系&#xff1a;重載&#xff08;Overload&#xff09;、覆蓋&#xff08;Override&#xff0…