跨域是個什麼鬼,你搞明白了嗎?

2021.11.08

跨域是個老生常談的話題了,最近不管在和後端聯調,或者搞微前端的時候都會遇到,正好寫篇文章來總結一下吧。


跨域是個老生常談的話題了,最近不管在和後端聯調,或者搞微前端的時候都會遇到,正好寫篇文章來總結一下吧。

 

跨域是什麼

這裡的跨域指的是不同源之間的資源訪問。只要請求的 url 有以下不同,都屬於跨域

 

·         協議: http, https, ...

·         域名

·         端口


有人可能會覺得,我自己網站肯定只訪問自己服務器,肯定都是部署在一個域名的呀。

 

但是有的時候,一個網頁可能要對接後端多個服務:一會對接支付,一會對接用戶信息。每個組的後端可能都會有自己的域名。在這樣的場景下,跨域就非常常見了。

 

為什麼會有跨域

我們常說的跨域問題,其實是在說跨域訪問的限制問題,相信大家對下面的報錯習以為常了:


這種跨域限制其實是 瀏覽器自帶的安全機制,只有 在瀏覽器上 發生跨域請求操作時,瀏覽器就會自動拋出上面的錯誤。

 

注意,這僅在瀏覽器上會出現這樣的限制,如果你用 Postman 這些工具訪問 url 是沒有跨域限制的,畢竟 Postman 連域名這些玩意都沒有,哪來的跨域

 

CORS

雖然瀏覽器出於安全考慮做了跨域訪問的限制,但開發時不可避免會有這樣不同源資源訪問的需求,因此 W3C 就制定了 CORS(Cross-origin resource sharing 跨域資源共享) 的機制。

 

很多人一直以為 CORS = 跨域,其實 CORS 是一種解決跨域的方案。

 

需要注意的是,CORS 是一個的協議(至少對於以前的 IE7 是新的),不僅需要瀏覽器支持,也後端服務器的支持。

 

瀏覽器支持沒什麼好說的,就是瀏覽器版本是否支持的問題:


然後就是後端服務器支持了,服務器需要在 Response Header 上添加 Access-Control-xxx-yyy 的字段,瀏覽器識別到了,才能放行該請求。比如,最常見的就是加 Access-Control-Allow-Origin 這個返回頭,值設置為需要放行的域名。

 

簡單請求 VS 非簡單請求

瀏覽器將 CORS 請求分為 簡單請求 非簡單請求。

 

簡單請求 會在發送時自動在 HTTP 請求頭加上 Origin 字段,來標明當前是哪個源(協議+域名+端口),服務端來決定是否放行。

 

非簡單請求 則會先發一個 OPTIONS 預檢請求給服務端,當通過了再發正常的 CORS 請求。

 

對於 簡單請求,請求方法為以下三種之一:

  • Head
  • Post
  • Get


HTTP 請求頭字段不能超過以下字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type


同時 Content-Type 只能三個值:

  • application/x-www-form-urlencoded 對應普通表單
  • multipart/form-data 對應文件上傳
  • text/plain 對應文本發送(一般不怎麼用)

 

只要不滿足上麵條件的,都屬於 非簡單請求。

 

可能很多人會自然地覺得 POST 請求都是 非簡單請求,因為我們常看到發 POST 時,都會先發 OPTIONS。其實是因為我們一般都會傳 JSON 格式的數據,Content-Type application/json,所以,這樣的 POST 請求才屬於 非簡單請求。

 

Access-Control-xxx-yyyy

CORS 請求為 簡單請求時,請求會檢測返回頭里的以下字段:

        ·         Access-Control-Allow-Origin:指定哪些源是可以共享資源的(包含協議、域名、端口)

        ·         Access-Control-Allow-Credentials:當請求需要攜帶 Cookie 時,需要加上這個字段為 true 才能允許攜帶 Cookie

        ·         Access-Control-Expose-Headers:由於 XMLHttpRequest 對像只能拿到 Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma 6 個基本字段。想要在拿到別的字段, 就需要在這裡去指定。

 

而當 CORS 請求為 非簡單請求時,瀏覽器會先發一個 OPTIONS 預檢(preflight)請求,這個請求會檢查如下字段:

        ·         Access-Control-Request-Method:指定可訪問的方法,對於非簡單請求,可能會用到 PUTPATCHDELETE RESTful 方法,服務端需要把這些方法名也加上。

        ·         Access-Control-Request-Headers:指定 HTTP 請求頭會額外添加的信息。一個很常見的場景就是,後端有時候會將一些環境參數放到請求頭里,如果不用這個字段來指定放行的字段,那麼就會出現跨域限制了。


如果 OPTIONS 請求沒有通過服務端的校驗,就會返回一個正常的 HTTP 請求,不會帶上 CORS 的返回信息,所以瀏覽器就會認定為跨域了。

 

總結一句話就是,當 Console 報哪個錯,你就在服務端返回頭上加上哪個字段就可以了。

 

CORS 中間件

無論對於 Express 還是 KOA,我們都不需要再手動添加上面的字段了,直接加一個 cors 中間件就可以很方便地添加上面的字段,寫起來也更優雅:

 

1.    var cors = require('cors'); 

2.     

3.    var corsOptions = { 

4.      origin: function (origin, callback) { 

5.        // db.loadOrigins is an example call to load 

6.        // a list of origins from a backing database 

7.        db.loadOrigins(function (error, origins) { 

8.          callback(error, origins) 

9.        }) 

10.   } 

11.

12.  

13. app.use('/', cors(corsOptions), indexRouter); 

 

JSONP

那對於瀏覽器不支持 CORS 的情況呢?雖然目前來看是不太可能,但是在還沒有 CORS 的時代,大家是怎麼解決跨域的呢?答案就是 JSONP

 

它的原理也非常簡單:雖然瀏覽器限制了 HTTP 的跨域,但是沒有限制獲取 script 標籤內容的跨域請求呀。

 

當我們插入一個 <script src="xxx.com"> 標籤的時候,會發一個獲取 xxx.com GET 請求,而這個 GET 請求又不存在跨域限制,通過這樣的方法就能解決跨域的問題了。

 

服務端實現:

1.    router.get('/', (req, res) =>  { 

2.      const { callback_name } = req.query; 

3.      res.send(`${callback_name}('hello')`) // 返回 JS 代码,调用 callback_name 这个函数,并传入 hello 

4.    }); 

前端实现:

1.    function jsonpCallback(params) { 

2.      alert('执行 public/index.html 里定义的 jsonpCallback 函数,并传入' + params + '参数'); 

3.   

4.     

5.     

6.    const jsonp = async () => { 

7.      // 制作 script 标签 

8.      const script = document.createElement('script'); 

9.      script.type = 'text/javascript'

10.   script.src = 'http://localhost:9000/user?callback_name=jsonpCallback' 

11.   // 添加标签 

12.   document.body.appendChild(script); 

13.   // 拿到数据再移除 

14.   document.body.removeChild(script); 

15.

16.  

17. jsonp(); 

 

當調用 jsonp 函數的時候,自動創建一個 script 標籤,再把請求放到 scr 裡,就會自動發起 GET 請求。服務端會直接返回一串 JavaScript 代碼,然後前端執行這段從服務端獲取來的 JS 代碼,獲取到後端數據。

 

跨域場景

跨域不僅存在於接口訪問,還會有以下場景:

  • 前端訪問跨域 URL,最常見的場景,需要後端添加 cors 的返回字段
  • 微前端:主應用和子應用之間的資源訪問可能存在跨域操作,需要子應用/主應用添加 cors
  • 登錄重定向:本質上和第一條一樣,不過在現象層面不太一樣。比如訪問 abc.com 時,有的網站會重定向到自己的登錄頁 passport.abc.com,如果 passport.abc.com 沒有設置 cors,也會出現跨域

 

總結

總的來說,我們常說的跨域,其實就是獲取不同源(協議+域名+端口)的資源時,瀏覽器自身 做出的限制。

 

在以前,開發者會用 JSONP 這種通過生成一個 script 標籤,自動發起 GET 請求的方式來解決跨域,但是這種方式非常不安全,不推薦。

 

到了現在,瀏覽器都已經完美支持 CORS 機制了,只需要在服務端添加對應的返回頭 Access-Control-xxx-yyy 就可以了。當瀏覽器報跨域錯誤時,缺哪個字段,就在服務端配哪個字段即可。

 

Node 端開發時,我們可以直接使用 cors 中間件來配置,就不用手寫返回頭里的字段了。