摘要:跨域正確的打開方式經過對同源策略的了解,我們應該要消除對瀏覽器的誤解,同源策略是瀏覽器做的一件好事,是用來防御來自邪門歪道的攻擊,但總不能為了不讓壞人進門而把全部人都拒之門外吧。
跨域這兩個字就像一塊狗皮膏藥一樣黏在每一個前端開發者身上,無論你在工作上或者面試中無可避免會遇到這個問題。為了應付面試,我每次都隨便背幾個方案,也不知道為什么要這樣干,反正面完就可以扔了,我想工作上也不會用到那么多亂七八糟的方案。到了真正工作,開發環境有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的加密通信中傳輸以防截獲。
1.有一天你剛睡醒,收到一封郵件,說是你的銀行賬號有風險,趕緊點進www.yinghang.com改密碼。你嚇尿了,趕緊點進去,還是熟悉的銀行登錄界面,你果斷輸入你的賬號密碼,登錄進去看看錢有沒有少了。
2.睡眼朦朧的你沒看清楚,平時訪問的銀行網站是www.yinhang.com,而現在訪問的是www.yinghang.com,這個釣魚網站做了什么呢?
// HTML // 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。
在HTML標簽里,一些標簽比如script、img這樣的獲取資源的標簽是沒有跨域限制的,利用這一點,我們可以這樣干:
后端寫個小接口
// 處理成功失敗返回格式的工具 const {successBody} = require("../utli") class CrossDomain { static async jsonp (ctx) { // 前端傳過來的參數 const query = ctx.request.query // 設置一個cookies ctx.cookies.set("tokenId", "1") // query.cb是前后端約定的方法名字,其實就是后端返回一個直接執行的方法給前端,由于前端是用script標簽發起的請求,所以返回了這個方法后相當于立馬執行,并且把要返回的數據放在方法的參數里。 ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, "success"))})` } } module.exports = CrossDomain
簡單版前端
簡單封裝一下前端這個套路
/** * JSONP請求工具 * @param url 請求的地址 * @param data 請求的參數 * @returns {Promise2.空iframe加form} */ 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}` }, "") } // 動態創建script標簽 const script = document.createElement("script") // 接口返回的數據獲取 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) })
細心的朋友可能發現,JSONP只能發GET請求,因為本質上script加載資源就是GET,那么如果要發POST請求怎么辦呢?
后端寫個小接口
// 處理成功失敗返回格式的工具 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}) => { // 首先創建一個用來發送數據的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中執行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() // 表單提交后,就可以刪除這個表單,不影響下次的數據發送. 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,且后端設置指定的origin ctx.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-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
一個接口就要寫這么多代碼,如果想所有接口都統一處理,有什么更優雅的方式呢?見下面的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`, { // 需要帶上cookie credentials: "include", // 這里添加額外的headers來觸發非簡單請求 headers: { "t": "extra headers" } }).then(res => { console.log(res) })4.代理
想一下,如果我們請求的時候還是用前端的域名,然后有個東西幫我們把這個請求轉發到真正的后端域名上,不就避免跨域了嗎?這時候,Nginx出場了。
Nginx配置
server{ # 監聽9099端口 listen 9099; # 域名是localhost server_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.postMessagewindow.postMessage() 是HTML5的一個接口,專注實現不同窗口不同頁面的跨域通訊。
為了演示方便,我們將hosts改一下:127.0.0.1 crossDomain.com,現在訪問域名crossDomain.com就等于訪問127.0.0.1。
這里是http://localhost:9099/#/crossDomain,發消息方
這里是http://crossdomain.com:9099,接收消息方
2.document.domain我是http://crossdomain.com:9099
這種方式只適合主域名相同,但子域名不同的iframe跨域。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,這種情況下給兩個頁面指定一下document.domain即document.domain = crossdomain.com就可以訪問各自的window對象了。
這個應該是一個比較冷門的跨域問題,張大神已經寫過了我就不再班門弄斧了解決canvas圖片getImageData,toDataURL跨域問題
最后
希望看完這篇文章之后,再有人問跨域的問題,你可以嘴角微微上揚,冷笑一聲:“不要再問我跨域的問題了。”
揚長而去。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/99826.html
摘要:前言騰訊一面,相比阿里一面來說,騰訊一面先給打電話預定時間,這也給了我們這些面試者去準備的時間。其實閉包也就是指有權訪問另一個函數作用域的函數而已。常用的創建閉包的方法就是在函數內部創建另一個函數。 前言 騰訊一面,相比阿里一面來說,騰訊一面先給打電話預定時間,這也給了我們這些面試者去準備的時間。但是也正是因為這種確定性,也有在等待電話的時候的心情的忐忑。 背景 我是一名大三學生,大一...
摘要:所謂同源是指協議域名端口三者相同,即便兩個不同的域名指向同一個地址,也非同源。那么怎樣解決跨域問題的呢通過跨域跨域跨域跨域跨域資源共享代理跨域中間件代理跨域音樂教程老師有用到協議跨域后端在頭部信息里面設置安全域名公司后端給解決過持續更新中 JavaScript篇 如何獲取瀏覽器URL中查詢字符串中的參數? 1.封裝方法 getUrlArgs(url) { const args =...
摘要:所謂同源是指協議域名端口三者相同,即便兩個不同的域名指向同一個地址,也非同源。那么怎樣解決跨域問題的呢通過跨域跨域跨域跨域跨域資源共享代理跨域中間件代理跨域音樂教程老師有用到協議跨域后端在頭部信息里面設置安全域名公司后端給解決過持續更新中 JavaScript篇 如何獲取瀏覽器URL中查詢字符串中的參數? 1.封裝方法 getUrlArgs(url) { const args =...
閱讀 3049·2021-09-22 15:52
閱讀 2914·2019-08-30 15:55
閱讀 2708·2019-08-30 15:53
閱讀 2461·2019-08-30 13:21
閱讀 1630·2019-08-30 13:10
閱讀 2488·2019-08-26 12:09
閱讀 2575·2019-08-26 10:33
閱讀 1810·2019-08-23 18:06