寫下這篇文章後我想,要不之後就把這種基礎的常見知識都歸到這個「不要再問我XX的問題」,造成一系列內容,但願你們看完以後再有人問你這些問題,你內心會竊喜:「嘿嘿,是時候展示真正的技術了!」
1、 不要再問我this的指向問題了
跨域這兩個字就像一塊狗皮膏藥同樣黏在每個前端開發者身上,不管你在工做上或者面試中無可避免會遇到這個問題。爲了應付面試,我每次都隨便背幾個方案,也不知道爲何要這樣幹,反正面完就能夠扔了,我想工做上也不會用到那麼多亂七八糟的方案。到了真正工做,開發環境有webpack-dev-server搞定,上線了服務端的大佬們也會配好,配了什麼我無論,反正不會跨域就是了。日子也就這麼混過去了,終於有一天,我以爲不能再繼續這樣混下去了,我必定要完全搞懂這個東西!因而就有了這篇文章。javascript
確實,咱們這種搬磚工人就是爲了混口飯吃嘛,好好的調個接口告訴我跨域了,這種阻礙咱們輕鬆搬磚的事情真噁心!爲何會跨域?是誰在搞事情?爲了找到這個問題的始做俑者,請點擊瀏覽器的同源策略。
這麼官方的東西真難懂,不要緊,至少你知道了,由於瀏覽器的同源策略致使了跨域,就是瀏覽器在搞事情。
因此,瀏覽器爲何要搞事情?就是不想給好日子咱們過?對於這樣的質問,瀏覽器甩鍋道:「同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。」
這麼官方的話術真難懂,不要緊,至少你知道了,彷佛這是個安全機制。
因此,究竟爲何須要這樣的安全機制?這樣的安全機制解決了什麼問題?別急,讓咱們繼續研究下去。html
據我瞭解,瀏覽器是從兩個方面去作這個同源策略的,一是針對接口的請求,二是針對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的加密通訊中傳輸以防截獲。vue
1.有一天你剛睡醒,收到一封郵件,說是你的銀行帳號有風險,趕忙點進www.yinghang.com改密碼。你嚇尿了,趕忙點進去,仍是熟悉的銀行登陸界面,你果斷輸入你的帳號密碼,登陸進去看看錢有沒有少了。
2.睡眼朦朧的你沒看清楚,平時訪問的銀行網站是www.yinhang.com,而如今訪問的是www.yinghang.com,這個釣魚網站作了什麼呢?java
// 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},我還拿不到你剛剛輸入的帳號密碼嗎`)
由此咱們知道,同源策略確實能規避一些危險,不是說有了同源策略就安全,只是說同源策略是一種瀏覽器最基本的安全機制,畢竟能提升一點攻擊的成本。其實沒有刺不穿的盾,只是攻擊的成本和攻擊成功後得到的利益成不成正比。node
通過對同源策略的瞭解,咱們應該要消除對瀏覽器的誤解,同源策略是瀏覽器作的一件好事,是用來防護來自邪門歪道的攻擊,但總不能爲了避免讓壞人進門而把所有人都拒之門外吧。沒錯,咱們這種正人君子只要打開方式正確,就應該能夠跨域。
下面將一個個演示正確打開方式,但在此以前,有些準備工做要作。爲了本地演示跨域,咱們須要:
1.隨便跑起一份前端代碼(如下前端是隨便跑起來的vue),地址是http://localhost:9099。
2.隨便跑起一份後端代碼(如下後端是隨便跑起來的node koa2),地址是http://localhost:9971。webpack
1.JSONP
在HTML標籤裏,一些標籤好比script、img這樣的獲取資源的標籤是沒有跨域限制的,利用這一點,咱們能夠這樣幹:web
後端寫個小接口面試
// 處理成功失敗返回格式的工具 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
簡單版前端json
<!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.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) })
2.空iframe加form
細心的朋友可能發現,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) 請求方法是如下三種方法之一:
(2)HTTP的頭信息不超出如下幾種字段:
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纔是更通用的作法吧。
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能夠做爲targetOrigin e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,這就是你想知道的結果:${document.getElementById('app') ? '有id爲app的Dom' : '沒有id爲app的Dom'}`, e.origin); } }) } } </script>
結果能夠看到:
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跨域問題
但願看完這篇文章以後,再有人問跨域的問題,你能夠嘴角微微上揚,冷笑一聲:「不要再問我跨域的問題了。」揚長而去。