【前端Tip】同源策略以及實現(xiàn)跨域請求

1. 同源策略和跨域

1.1 什么是同源

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

image.png

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之間可以相互訪問,那么黑客可以這樣進行攻擊:

  1. 做一個假網(wǎng)站,里面用iframe嵌套一個銀行網(wǎng)站:http://testbank.com
  2. 把iframe寬高調(diào)整到頁面全部,這樣用戶進來之后除了域名,別的部分和銀行的網(wǎng)站沒有任何區(qū)別;
  3. 如果用戶輸入賬號密碼,我們的主網(wǎng)站可以跨域訪問到http://testbank.com的dom節(jié)點,就可以拿到用戶的賬戶密碼了;

1.3.2 如果沒有XMLHttpRequest同源策略

如果沒有XMLHttpRequest同源策略,那么黑客可以進行CSRF(跨站請求偽造)攻擊:

  1. 用戶登錄了自己的銀行頁面http://testbank.com,向用戶的cookie中添加用戶標識;
  2. 用戶瀏覽了惡意頁面 http://evil.com,執(zhí)行了頁面中的AJAX請求代碼;
  3. 請求中會默認攜帶cookie;
  4. 銀行頁面從發(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

下圖中展示的是瀏覽器對跨域請求的攔截:


image.png

從上圖中我們可以看到,瀏覽器是允許發(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屬性進行跨域。

  1. 事先定義一個用于獲取跨域響應數(shù)據(jù)的回調(diào)函數(shù);
  2. 創(chuàng)建一個script標簽發(fā)起一個請求(將回調(diào)函數(shù)的名稱作為參數(shù)放到這個請求的query參數(shù)里);
  3. 服務端獲取到這個請求之后,獲取query中的回調(diào)函數(shù)的名稱,并將數(shù)據(jù)放到回調(diào)函數(shù)的參數(shù)里,作為請求的響應;
  4. 前端的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;
    }
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容