1. 同源策略和跨域
1.1 什么是同源
如果兩個頁面的協(xié)議、域名和端口都相同,則說明兩個頁面具有相同的源。比如下面的圖片,給出了相對于http://www.test.com/index.html頁面的同源監(jiān)測:

1.2 什么是同源策略
同源策略(Same origin policy)是一種約定,它是瀏覽器最核心也是最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能可能都會收到影響??梢哉fWeb是構(gòu)建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現(xiàn)。
它的核心就在于它認為從任何站點裝載的信息內(nèi)容都是不安全的,它們應該只被允許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源。
另外,同源策略分為以下兩種:
DOM同源策略:
禁止對不同源頁面DOM進行操作。這里主要場景是iframe跨域的情況,不同域名的iframe是限制互相訪問的;XMLHttpRequest同源策略:
禁止使用XHR對象向不同源的服務器地址發(fā)起HTTP請求;
1.3 為什么要有跨域限制
因為存在瀏覽器同源策略,所以才會有跨域問題。那么瀏覽器是出于何種原因會有跨域的限制呢?其實很簡單,就是為了用戶的上網(wǎng)安全。
1.3.1 如果沒有DOM同源策略
如果沒有DOM同源策略,也就是說不同域的iframe之間可以相互訪問,那么黑客可以這樣進行攻擊:
- 做一個假網(wǎng)站,里面用iframe嵌套一個銀行網(wǎng)站:
http://testbank.com - 把iframe寬高調(diào)整到頁面全部,這樣用戶進來之后除了域名,別的部分和銀行的網(wǎng)站沒有任何區(qū)別;
- 如果用戶輸入賬號密碼,我們的主網(wǎng)站可以跨域訪問到
http://testbank.com的dom節(jié)點,就可以拿到用戶的賬戶密碼了;
1.3.2 如果沒有XMLHttpRequest同源策略
如果沒有XMLHttpRequest同源策略,那么黑客可以進行CSRF(跨站請求偽造)攻擊:
- 用戶登錄了自己的銀行頁面
http://testbank.com,向用戶的cookie中添加用戶標識; - 用戶瀏覽了惡意頁面
http://evil.com,執(zhí)行了頁面中的AJAX請求代碼; - 請求中會默認攜帶cookie;
- 銀行頁面從發(fā)送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數(shù)據(jù),此時數(shù)據(jù)就泄露了。由于Ajax在后臺執(zhí)行,用戶無法感知這一過程;
因此,有了瀏覽器的同源策略,才能讓我們更安全的上網(wǎng)。
1.3.3 跨域
從上面我們了解到了瀏覽器同源策略的作用,也正是有了跨域限制,才使我們能安全的上網(wǎng)。但是在實際開發(fā)中,我們需要突破這樣的限制。
出現(xiàn)跨域的根本原因是:瀏覽器的同源策略不允許非同源的URL之間進行資源的交互。
比如:
當前的網(wǎng)頁是:http://www.test.com/index.html
請求的接口是:http:www.api.com/userlist
下圖中展示的是瀏覽器對跨域請求的攔截:

從上圖中我們可以看到,瀏覽器是允許發(fā)起請求,但是,跨域請求回來的數(shù)據(jù),會被瀏覽器攔截,無法被頁面獲取到。
2. 如何實現(xiàn)跨域請求
實現(xiàn)跨域數(shù)據(jù)請求方法有很多,比如JSONP、CORS、使用Proxy等。
JSONP:
出現(xiàn)的早,兼容性好(兼容低版本IE)。是前端程序員為了解決跨域問題,被迫想出來的一種臨時解決方案。缺點是只支持 GET 請求,不支持 POST 請求;CORS:
出現(xiàn)的較晚,它是 W3C 標準,屬于跨域 AJAX 請求的根本解決方案。支持 GET 和 POST 請求。缺點是不兼容某些低版本的瀏覽器。Proxy:
既然跨域是瀏覽器導致的,那我們可以使用代理繞開瀏覽器,這也是常見的解決跨域的方案;
3. JSONP
JSONP(JSON with Padding)是JSON的一種使用模式,可用于解決主流瀏覽器跨域數(shù)據(jù)訪問的問題。
3.1 JSONP原理
由于script標簽不受瀏覽器同源策略的影響,允許跨域引用資源。因此,可以通過動態(tài)創(chuàng)建script標簽,然后利用src屬性進行跨域。
- 事先定義一個用于獲取跨域響應數(shù)據(jù)的回調(diào)函數(shù);
- 創(chuàng)建一個script標簽發(fā)起一個請求(將回調(diào)函數(shù)的名稱作為參數(shù)放到這個請求的query參數(shù)里);
- 服務端獲取到這個請求之后,獲取query中的回調(diào)函數(shù)的名稱,并將數(shù)據(jù)放到回調(diào)函數(shù)的參數(shù)里,作為請求的響應;
- 前端的script標簽收到請求的響應之后,會立馬執(zhí)行這個回調(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.定義一個回調(diào)函數(shù),用來接收返回的數(shù)據(jù)
function getData(data) {
console.log('data :>> ', data);
}
</script>
<!-- 2.使用script標簽(也可以動態(tài)創(chuàng)建),并且告訴后端回調(diào)函數(shù)的名字是getData -->
<!-- 3.通過script.src 請求 http://localhost:8080/api/data?cb=getData-->
<!-- 4.后端識別這樣的url格式并處理該請求,然后返回 getData('hello') 給瀏覽器 -->
<!-- 5.瀏覽器在接收到 getData('hello') 數(shù)據(jù)之后,會執(zhí)行 getData 方法,獲得后端返回的數(shù)據(jù) -->
<script src="http://localhost:8080/api/data?cb=getData"></script>
</body>
</html>
3.2.2 后端響應示例
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)/缺點
優(yōu)點:
- 使用簡單,沒有兼容性問題;
- 請求完畢之后可以通過調(diào)用callback的方式回傳結(jié)果;
缺點:
- 只支持GET請求,不支持POST等其它類型的請求;
- 由于是從其它域中加載代碼執(zhí)行,因此如果其他域不安全,很可能會在響應中夾帶一些惡意代碼;
- 只支持HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript調(diào)用的問題;
- 要確定JSONP請求是否失敗并不容易,雖然H5給script標簽新增了一個onerror事件處理程序,但是存在兼容性問題;
4.CORS
之前在學習OPTIONS預檢請求的時候,已經(jīng)總結(jié)過了。詳見:http://www.itdecent.cn/p/d9d30dc9898b
5. Proxy
簡單來說,就是請求自己同源的服務(代理),然后通過代理去請求跨域的資源。常用的解決方案一般是兩種:本地代理和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;
}
}