前言?
前端關(guān)于網(wǎng)絡安全看似高深莫測,其實來來回回就那么點東西,我總結(jié)一下就是 3 + 1 ?= 4,3個用字母描述的【分別是 XSS、CSRF、CORS】 + 一個中間人攻擊。當然 CORS 同源策略是為了防止攻擊的安全策略,其他的都是網(wǎng)絡攻擊。除了這 4 個前端相關(guān)的面試題,其他的都是一些不常用的小嘍啰。
我將會在我的《面試題一網(wǎng)打盡》專欄中先逐一詳細介紹,然后再來一篇文章總結(jié),預計一共5篇文章,歡迎大家關(guān)注~
本篇文章是前端網(wǎng)絡安全相關(guān)的第三篇文章,內(nèi)容就是 CORS 同源策略。
一、準備工作?
1.1 拉取倉庫?
本篇文章的基礎(chǔ)是需要一個服務端的項目,可以跟著我的這篇文章搭建自己的服務端項目?;蛘咧苯涌寺∥业膫}庫代碼在這個提交上拉一個新分支,本篇文章所有的代碼都是在這個提交基礎(chǔ)上進行的。
在本篇文章之前,我已經(jīng)寫了 xss 攻擊和 csrf 攻擊的文章,所以在你拉取我的 git 最新代碼的時候,已經(jīng)有很多更新的提交了。不過,無論是從上面我說的那個提交拉取新分支,還是拉取最新的代碼都可以,我的倉庫的所有的合并都是相互獨立的。
不論你先看 XSS 教程還是先看 CSRF 教程,還是這篇關(guān)于跨域的文章都可以。
1.2 新增 CORS 文件夾
二、同源策略
兩個網(wǎng)站協(xié)議名、域名、端口號有一個不同就是非同源,就是跨域。跨域問題就是瀏覽器的同源策略造成的。
同源是指協(xié)議名、域名、端口號 必須完全一致!
http 默認端口號是80,https 默認端口號是443
2.1 同源策略的限制
一般來說,同源策略是指對 javascript 腳本的限制,
- js 腳本不能跨域訪問 cookie、localstorage、indexDB
- js 腳本不能跨域操作 dom
- 不能跨域發(fā)送 ajax 請求
三、CORS 解決跨域問題
3.1 簡單請求
簡單請求不會發(fā)生跨域 cors 預檢請求,預檢請求 Preflight Request 是用于驗證是否允許非簡單請求的一種 OPTIONS 請求。預檢請求指示為了減少跨域請求的復雜性和延遲,不是說簡單請求就一定不會報跨域錯誤。而是非簡單請求跨域的概率大一些,所以要預檢。預檢請求是 CORS 機制的一部分,用于確??缬蛘埱蟮陌踩裕A檢失敗,不會發(fā)送實際的跨域請求。
3.1.1 簡單請求的條件
(1)head、get、post是這三種方法之一【注意,我們常見的post請求不會發(fā)送預檢請求】或者
(2)沒有自定義 http 請求頭,除了下面的字段
- Accept
- Accept-Language
- Content-Language
- Content-Type【只允許3個類型】
- Range
話雖然這樣說,但是實際的簡單請求頭還有很多字段,比如 orign 、host等【如下圖也是一個簡單請求的頭部】這些字段是瀏覽器自動設置的。下面的 cache-control 也是瀏覽器自動加的。
(3)content-type 僅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
注意這三個條件是或的關(guān)系,如果 get 請求加上了自定義的請求頭,那么就不是簡單請求了?;蛘撸唵握埱蟮?content-type 設置了其他值也就不是簡單請求了。簡單請求的跨域請求不會發(fā)送預檢請求。
mdn 官網(wǎng)都有說呢
多說無益,直接寫代碼,你會更好理解。我們看看什么樣的情況會有預檢請求。
3.1.2?搭建 cors 服務
(1)新建 cors/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
cors
</body>
</html>
(2)新建 cors/index.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
app.listen(3000);
(3)運行 npm run dev cors
(4)提交代碼
3.1.3??設置 get 非簡單請求?
我們要創(chuàng)造一個跨域的請求,但是我們只有一個服務,其實也很簡單,那就是使用 localhost 去訪問 ip 地址,自然就跨域了。
(1)get 簡單請求
我們使用 fetch 只寫一個簡單的 get 請求,,會發(fā)現(xiàn)沒有請求列表中沒有多余的請求
(2)get 自定義請求頭
我們給請求加上自定義的請求頭,就會多出一個預檢請求
同理,對于head、post 請求如果我們不加自定義響應頭,也不會有預檢請求
(3)get 設置 content-type
請求頭設置了除了?application/x-www-form-urlencoded、multipart/form-data、text/plain 這三個值之外,都會發(fā)送預檢請求,可以自己測試一下。
content-type 有很多取值,可以自己看一下官方文檔。
我們需要記住幾個比較常見的
application/json
: 用于指示請求或響應中的實體是 JSON 格式的數(shù)據(jù)。
application/xml
: 用于指示請求或響應中的實體是 XML 格式的數(shù)據(jù)。
text/html
: 用于指示請求或響應中的實體是 HTML 格式的文本。
text/plain
: 用于指示請求或響應中的實體是純文本,沒有特定的格式?!竞唵握埱蟆?/span>
multipart/form-data
: 用于指示請求中包含多個部分,通常用于文件上傳。【簡單請求】
application/x-www-form-urlencoded
: 用于指示請求中的數(shù)據(jù)是 URL 編碼的表單數(shù)據(jù),通常用于普通表單提交?!竞唵握埱蟆?/span>
application/octet-stream
: 用于指示請求或響應中的實體是二進制流,可以是任意類型的數(shù)據(jù)。
image/jpeg
,image/png
,image/gif
, 等: 用于指示請求或響應中的實體是圖片文件,具體的媒體類型根據(jù)具體的圖片格式而定。這些只是一些常見的
Content-Type
取值,實際上還有許多其他可能的值,取決于你要傳輸?shù)臄?shù)據(jù)類型。當你發(fā)送 HTTP 請求時,通過設置適當?shù)?Content-Type
,可以確保服務器能夠正確地解析請求體中的數(shù)據(jù)。同樣,在處理 HTTP 響應時,Content-Type
頭部指示了響應中實體的類型,幫助客戶端正確解析響應的內(nèi)容
3.1.4 put、delete 非簡單請求
?put 或者 delete等 請求,無論有沒有自定義的響應頭,無論有沒有設置 content-type 都會發(fā)送預檢請求。
再次強調(diào)一下,簡單請求還是非簡單請求,我們研究的前提都是對于跨域請求的,對于非跨域的請求,是沒有預檢這一說的。?
?3.2? CORS 跨域資源共享
現(xiàn)在我們解決這個跨域的錯誤,對于跨域問題前端的同學其實不用做什么操作的,主要還是服務端的配置。
?3.2.1 安裝 cors
pnpm i cors
我們使用的 express 實現(xiàn)跨域,可以直接安裝 cors 這個包
3.2.2 修改 index.js
就這么簡單,就不會有跨域問題了。
我們使用 cors 這個 npm 包就不用手動配置 http 的響應頭了。
?
這里面有一個知識點,就是 http 請求響應的狀態(tài)碼是204
204 No Content
對于該請求沒有的內(nèi)容可發(fā)送,但頭部字段可能有用。用戶代理可能會用此時請求頭部信息來更新原來資源的頭部緩存字段。
3.2.3 提交代碼
3.3 自定義響應頭
如果我們不使用 cors 這個包,我們就需要自定義響應頭,關(guān)于跨域的響應頭主要有三個,分別是:
- Access-Control-Allow-Origin 允許的 origin
- Access-Control-Allow-Methods 允許的方法
- Access-Control-Allow-Headers 允許的請求頭
3.3.1 設置跨域
這里面有個關(guān)于 express 的知識點,就是 express 自定義跨域響應頭的時候要使用中間件【app.use】,如果你直接在請求中設置是不會生效的,因為跨域請求有一個預檢請求。
3.3.2 提交代碼
3.4 攜帶憑證
3.4.1 前端攜帶 cookie
在Web開發(fā)中,"攜帶憑證"(Credentials)是指允許在跨域請求中發(fā)送和接收帶有身份驗證信息的請求。身份驗證信息通常包括使用Cookie、HTTP認證或客戶端證書等方式進行的用戶認證。
我們常說的攜帶憑證其實就是攜帶 cookie
我們在前端需要修改 index.html
關(guān)于 fetch 的使用,可以看官網(wǎng)
只設置前端是不夠的,會報錯,服務端需要配置另外一個響應頭。
3.4.2 服務端允許攜帶 cookie?
?這樣就可以完美的攜帶cookie了
3.4.3 提交代碼
四、JSONP 實現(xiàn)跨域
JSONP 就是 JSON with padding,是一種跨域通信技術(shù),為了解決腳本跨域的問題
4.1 JSONP原理
利用 script 標簽的跨域特性,script 標簽是可以跨域的,在頁面中插入一個指向跨域資源的 script 標簽,以回調(diào)函數(shù)的形式返回數(shù)據(jù)。服務器返回的數(shù)據(jù)被包裝在這個回調(diào)函數(shù)中,使得跨域請求的數(shù)據(jù)能夠被當前頁面取到。
重點如下
- 利用 script 標簽
- 在 script 標簽 的 src 中加上 callback 參數(shù)函數(shù)
- 前端定義 callback 函數(shù),并把他的名稱拼接在 script 標簽的 src 上
- 服務器獲取 src 上的參數(shù)函數(shù),并調(diào)用這個函數(shù),給函數(shù)傳遞參數(shù),服務端返回就會執(zhí)行這個函數(shù),注意到了沒有?這個理論邏輯和反射型 xss 攻擊是一樣的,服務端返回 js 代碼,瀏覽器會自動執(zhí)行。
- 后端調(diào)用完參數(shù),前端就自動執(zhí)行了定義的函數(shù)
- jsonp 只支持方法
- 可能會受到 xss 攻擊,所以已經(jīng)被 cors 所取代
注意到?jīng)]有,這塊和我們之前寫的 XSS 攻擊的原理類似,具體可以看下這篇文章,里面詳細講解了各種類型的 XSS 攻擊。
4.2 代碼實現(xiàn)
4.2.1 搭建jsonp 服務器
4.2.2 修改 index.js
const express = require('express');
const path = require('path')
const app = express();
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
app.get('/data', function (req, res) {
const { query } = req
// 獲取參數(shù)上的回調(diào)
const callback = query.callback
// 服務端返回執(zhí)行回調(diào)函數(shù),并傳遞參數(shù) { name: 'test' }
res.send(`${callback}({ name: 'test' })`)
});
app.listen(3000);
4.2.3 修改 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
jsonp
<script>
function handleData(data) {
console.log('jsonp 返回的值', data);
// 跨域請求,服務端,沒有配置可以跨域
fetch('http://10.10.25.120:3000/data')
}
</script>
<!-- 這個腳本地址要設置成跨域的-->
<script src="http://10.10.25.120:3000/data?callback=handleData" ></script>
</body>
</html>
4.2.4 運行
npm run dev jsonp
jsonp 發(fā)起跨域請求的方式已經(jīng)很少用了,已經(jīng)被 CORS 所取代了。
4.2.5 提交代碼
五、其他的跨域方式
5.1 postMessage
- 頁面和其打開的新窗口傳遞數(shù)據(jù)
- 多窗口之間消息傳遞
- 頁面與嵌套的 iframe 消息傳遞
- iframe.contentWindow.postMessage + window.addEventListener
具體使用方法,可以參考這篇文章
5.2?服務器代理
- 可以自己定義個 express 服務器進行代理,使用 node + express + http-proxy-middleware 等插件
- nginx 做反向代理【反向代理是服務端的代理隱藏真實服務器,正式代理是客戶端的代理隱藏客戶端】
- vue 框架 node + vue + webpack + webpack-dev-server在 config 文件中配置 devServers,原理是利用 http-proxy-middleware + express 這個http代理中間件
- vite 框架 config 文件中也可以配置 server proxy
- websocket 沒有瀏覽器跨域的限制,因為它基于 tcp 協(xié)議
- EventSource 基于http,所以會跨域,需要服務端設置請求頭。
這里面有個概念,正向代理 vs 反向代理
一句話總結(jié),對客戶端的代理是正向代理,對服務端的代理是反向代理,【客戶是正面的
~~】
正向代理和反向代理的區(qū)別-CSDN博客
5.3?express 實現(xiàn)反向代理
我們接下來用 expess 來實現(xiàn)以下反向代理,對服務端的代理。
應用場景:假設后端給了一個服務地址https://a.com,但是后端這個服務沒有設置允許跨域,你要自己調(diào)試的時候,就可以自己實現(xiàn)一個本地的、非跨域的服務,然后代理后端的地址。你本地的前端訪問你本地的服務http://localhost:3000,你本地的服務再把請求轉(zhuǎn)發(fā)給后端的地址。
這里面有個知識點,服務端之間進行請求是不存在跨域的,跨域只針對前端和服務端,因為跨域是瀏覽器的同源策略,需要有瀏覽器參與,才有跨域問題。
5.3.1 安裝?http-proxy-middleware
pnpm i http-proxy-middleware
5.3.2 搭建 proxy 服務
在根目錄下新建? proxy 文件夾,并新建 proxy/index.html 文件、proxy/index.js
假設,本地服務 localhost:3000, 本來需要訪問 localhost:3001,但是跨域,因為 localhost:3001的服務沒有設置允許跨域;所以可以? localhost:3000 訪問? localhost:3000的非跨域服務;同時? localhost:3000 服務在服務器端對 3001 端口進行反向代理。
(1)index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
proxy
<script>
// 頁面地址 localhost:3000
// 本來需要請求 localhost:3001,但是跨域,
// 所以請求 localhost:3000/api/info 進行代理
function init () {
// 跨域
fetch('http://localhost:3001/api/info').then((res) => {
console.log('請求成功', res)
}).catch(() => {
console.log('請求失敗')
})
// 代理
fetch('/api/info').then((res) => {
console.log('請求成功', res)
})
}
init()
</script>
</body>
</html>
(2)index.js
const express = require('express');
const path = require('path')
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
// 把針對 /api 的請求,轉(zhuǎn)發(fā)給 3001 端口的服務為
app.use('/api', createProxyMiddleware({
target: 'http://localhost:3001',
timeout: 3000,
changeOrigin: true,
}))
app.listen(3000);
// 第二個服務 3001 端口,未配置允許跨域請求
const app1 = express();
app1.get('/api/info', function(req, res){
res.send('proxy ok')
})
app1.listen(3001)
(3)運行結(jié)果
npm run dev proxy
總結(jié)
關(guān)于跨域的問題已經(jīng)總結(jié)完成,本篇文章詳細介紹了如何使用并配置CORS、JSONP 的實現(xiàn)、Express 進行反向代理。
我的倉庫地址如下,歡迎查看
yangjihong2113/learn-express
內(nèi)容較多,難免疏漏,如有問題,歡迎指正。文章來源:http://www.zghlxwxcb.cn/news/detail-790418.html
這是一系列的文章,續(xù)更新中,歡迎關(guān)注。文章來源地址http://www.zghlxwxcb.cn/news/detail-790418.html
到了這里,關(guān)于徹底理解前端安全面試題(3)—— CORS跨域資源共享,解決跨域問題,建議收藏(含源碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!