1. 同源策略和跨域
1.1 什么是同源
如果兩個(gè)頁面的協(xié)議
选浑、域名
和端口
都相同惩歉,則說明兩個(gè)頁面具有相同的源激率。比如下面的圖片娄帖,給出了相對于http://www.test.com/index.html
頁面的同源監(jiān)測:
1.2 什么是同源策略
同源策略(Same origin policy)
是一種約定姻成,它是瀏覽器最核心也是最基本的安全功能插龄,如果缺少了同源策略,則瀏覽器的正常功能可能都會(huì)收到影響科展【危可以說Web是構(gòu)建在同源策略基礎(chǔ)之上的,瀏覽器只是針對同源策略的一種實(shí)現(xiàn)才睹。
它的核心就在于它認(rèn)為從任何站點(diǎn)裝載的信息內(nèi)容都是不安全的徘跪,它們應(yīng)該只被允許訪問來自同一站點(diǎn)的資源,而不是那些來自其它站點(diǎn)可能懷有惡意的資源琅攘。
另外垮庐,同源策略分為以下兩種:
DOM同源策略:
禁止對不同源頁面DOM進(jìn)行操作。這里主要場景是iframe跨域的情況乎澄,不同域名的iframe是限制互相訪問的突硝;XMLHttpRequest同源策略:
禁止使用XHR對象向不同源的服務(wù)器地址發(fā)起HTTP請求;
1.3 為什么要有跨域限制
因?yàn)榇嬖跒g覽器同源策略置济,所以才會(huì)有跨域問題解恰。那么瀏覽器是出于何種原因會(huì)有跨域的限制呢锋八?其實(shí)很簡單,就是為了用戶的上網(wǎng)安全护盈。
1.3.1 如果沒有DOM同源策略
如果沒有DOM同源策略挟纱,也就是說不同域的iframe之間可以相互訪問,那么黑客可以這樣進(jìn)行攻擊:
- 做一個(gè)假網(wǎng)站腐宋,里面用iframe嵌套一個(gè)銀行網(wǎng)站:
http://testbank.com
- 把iframe寬高調(diào)整到頁面全部紊服,這樣用戶進(jìn)來之后除了域名,別的部分和銀行的網(wǎng)站沒有任何區(qū)別胸竞;
- 如果用戶輸入賬號(hào)密碼欺嗤,我們的主網(wǎng)站可以跨域訪問到
http://testbank.com
的dom節(jié)點(diǎn),就可以拿到用戶的賬戶密碼了卫枝;
1.3.2 如果沒有XMLHttpRequest同源策略
如果沒有XMLHttpRequest
同源策略煎饼,那么黑客可以進(jìn)行CSRF
(跨站請求偽造)攻擊:
- 用戶登錄了自己的銀行頁面
http://testbank.com
,向用戶的cookie中添加用戶標(biāo)識(shí)校赤; - 用戶瀏覽了惡意頁面
http://evil.com
吆玖,執(zhí)行了頁面中的AJAX請求代碼; - 請求中會(huì)默認(rèn)攜帶cookie马篮;
- 銀行頁面從發(fā)送的cookie中提取用戶標(biāo)識(shí)沾乘,驗(yàn)證用戶無誤,response中返回請求數(shù)據(jù)浑测,此時(shí)數(shù)據(jù)就泄露了翅阵。由于Ajax在后臺(tái)執(zhí)行,用戶無法感知這一過程尽爆;
因此怎顾,有了瀏覽器的同源策略读慎,才能讓我們更安全的上網(wǎng)漱贱。
1.3.3 跨域
從上面我們了解到了瀏覽器同源策略的作用,也正是有了跨域限制夭委,才使我們能安全的上網(wǎng)幅狮。但是在實(shí)際開發(fā)中,我們需要突破這樣的限制株灸。
出現(xiàn)跨域的根本原因是:瀏覽器的同源策略不允許非同源的URL之間進(jìn)行資源的交互崇摄。
比如:
當(dāng)前的網(wǎng)頁是:http://www.test.com/index.html
請求的接口是:http:www.api.com/userlist
下圖中展示的是瀏覽器對跨域請求的攔截:
從上圖中我們可以看到,瀏覽器是允許發(fā)起請求慌烧,但是逐抑,跨域請求回來的數(shù)據(jù),會(huì)被瀏覽器攔截屹蚊,無法被頁面獲取到厕氨。
2. 如何實(shí)現(xiàn)跨域請求
實(shí)現(xiàn)跨域數(shù)據(jù)請求方法有很多进每,比如JSONP
、CORS
命斧、使用Proxy
等田晚。
JSONP:
出現(xiàn)的早,兼容性好(兼容低版本IE)国葬。是前端程序員為了解決跨域問題贤徒,被迫想出來的一種臨時(shí)解決方案。缺點(diǎn)是只支持 GET 請求汇四,不支持 POST 請求接奈;CORS:
出現(xiàn)的較晚,它是 W3C 標(biāo)準(zhǔn)通孽,屬于跨域 AJAX 請求的根本解決方案鲫趁。支持 GET 和 POST 請求。缺點(diǎn)是不兼容某些低版本的瀏覽器利虫。Proxy:
既然跨域是瀏覽器導(dǎo)致的挨厚,那我們可以使用代理繞開瀏覽器,這也是常見的解決跨域的方案糠惫;
3. JSONP
JSONP(JSON with Padding)是JSON的一種使用模式疫剃,可用于解決主流瀏覽器跨域數(shù)據(jù)訪問的問題。
3.1 JSONP原理
由于script標(biāo)簽不受瀏覽器同源策略的影響硼讽,允許跨域引用資源巢价。因此,可以通過動(dòng)態(tài)創(chuàng)建script標(biāo)簽固阁,然后利用src屬性進(jìn)行跨域壤躲。
- 事先定義一個(gè)用于獲取跨域響應(yīng)數(shù)據(jù)的回調(diào)函數(shù);
- 創(chuàng)建一個(gè)script標(biāo)簽發(fā)起一個(gè)請求(將回調(diào)函數(shù)的名稱作為參數(shù)放到這個(gè)請求的query參數(shù)里)备燃;
- 服務(wù)端獲取到這個(gè)請求之后碉克,獲取query中的回調(diào)函數(shù)的名稱,并將數(shù)據(jù)放到回調(diào)函數(shù)的參數(shù)里并齐,作為請求的響應(yīng)漏麦;
- 前端的script標(biāo)簽收到請求的響應(yīng)之后,會(huì)立馬執(zhí)行這個(gè)回調(diào)函數(shù)况褪,于是撕贞,就可以在之前定義的回調(diào)函數(shù)中獲取到數(shù)據(jù)了;
3.2 示例
3.2.1 前端請求示例
<!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>
<script>
// 1.定義一個(gè)回調(diào)函數(shù)测垛,用來接收返回的數(shù)據(jù)
function getData(data) {
console.log('data :>> ', data);
}
</script>
<!-- 2.使用script標(biāo)簽(也可以動(dòng)態(tài)創(chuàng)建)捏膨,并且告訴后端回調(diào)函數(shù)的名字是getData -->
<!-- 3.通過script.src 請求 http://localhost:8080/api/data?cb=getData-->
<!-- 4.后端識(shí)別這樣的url格式并處理該請求,然后返回 getData('hello') 給瀏覽器 -->
<!-- 5.瀏覽器在接收到 getData('hello') 數(shù)據(jù)之后食侮,會(huì)執(zhí)行 getData 方法号涯,獲得后端返回的數(shù)據(jù) -->
<script src="http://localhost:8080/api/data?cb=getData"></script>
</body>
</html>
3.2.2 后端響應(yīng)示例
const http = require('http')
const url = require('url')
const server = http.createServer((req, res)=>{
let urlString = req.url
let urlObj = url.parse(urlString, true)
res.write(`${urlObj.query.cb}("hello")`)
res.end()
})
server.listen(8080, () => {
console.log("localhost:8080");
})
3.3 優(yōu)/缺點(diǎn)
優(yōu)點(diǎn):
- 使用簡單熬北,沒有兼容性問題;
- 請求完畢之后可以通過調(diào)用callback的方式回傳結(jié)果诚隙;
缺點(diǎn):
- 只支持GET請求讶隐,不支持POST等其它類型的請求;
- 由于是從其它域中加載代碼執(zhí)行久又,因此如果其他域不安全巫延,很可能會(huì)在響應(yīng)中夾帶一些惡意代碼;
- 只支持HTTP請求這種情況地消,不能解決不同域的兩個(gè)頁面之間如何進(jìn)行JavaScript調(diào)用的問題炉峰;
- 要確定JSONP請求是否失敗并不容易,雖然H5給script標(biāo)簽新增了一個(gè)onerror事件處理程序脉执,但是存在兼容性問題疼阔;
4.CORS
之前在學(xué)習(xí)OPTIONS預(yù)檢請求的時(shí)候,已經(jīng)總結(jié)過了半夷。詳見:http://www.reibang.com/p/d9d30dc9898b
5. Proxy
簡單來說婆廊,就是請求自己同源的服務(wù)(代理),然后通過代理去請求跨域的資源巫橄。常用的解決方案一般是兩種:本地代理和nginx反向代理淘邻。
5.1 本地代理
開發(fā)環(huán)境,前端處理湘换。
無論是 webpack 還是 vite 都內(nèi)置了本地代理宾舅。這讓我們能夠在不依賴后端的前提下解決跨域的問題(僅僅是本地開發(fā)環(huán)境下, 線上環(huán)境需要 nginx 配置反向代理)
webpack的處理方式如下:
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
vite的處理方式:
export default defineConfig({
// ...
server: {
proxy: {
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
5.2 nginx反向代理
生產(chǎn)環(huán)境一般用 nginx 托管部署我們的前端代碼包。處理跨域問題需要 nginx 配置反向代理彩倚。
server {
listen: 8001;
server_name 10.2.2.25;
location ~ /api/ {
proxy_pass http://127.0.0.1:8081;
}
}