跨域是個什么鬼,你搞明白了嗎?
跨域是個老生常談的話題了,最近不管在和后端聯調,或者搞微前端的時候都會遇到,正好寫篇文章來總結一下吧。
跨域是什么
這里的“跨域”指的是不同源之間的資源訪問。只要請求的 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-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 這 6 個基本字段。想要在拿到別的字段,就需要在這里去指定。
而當 CORS 請求為 非簡單請求時,瀏覽器會先發一個 OPTIONS 預檢(preflight)請求,這個請求會檢查如下字段:
- Access-Control-Request-Method:指定可訪問的方法,對于非簡單請求,可能會用到 PUT,PATCH,DELETE 等 RESTful 方法,服務端需要把這些方法名也加上。
- Access-Control-Request-Headers:指定 HTTP 請求頭會額外添加的信息。一個很常見的場景就是,后端有時候會將一些環境參數放到請求頭里,如果不用這個字段來指定放行的字段,那么就會出現“跨域”限制了。
如果 OPTIONS 請求沒有通過服務端的校驗,就會返回一個正常的 HTTP 請求,不會帶上 CORS 的返回信息,所以瀏覽器就會認定為“跨域”了。
總結一句話就是,當 Console 報哪個錯,你就在服務端返回頭上加上哪個字段就可以了。
CORS 中間件
無論對于 Express 還是 KOA,我們都不需要再手動添加上面的字段了,直接加一個 cors 中間件就可以很方便地添加上面的字段,寫起來也更優雅:
- var cors = require('cors');
- var corsOptions = {
- origin: function (origin, callback) {
- // db.loadOrigins is an example call to load
- // a list of origins from a backing database
- db.loadOrigins(function (error, origins) {
- callback(error, origins)
- })
- }
- }
- app.use('/', cors(corsOptions), indexRouter);
JSONP
那對于瀏覽器不支持 CORS 的情況呢?雖然目前來看是不太可能,但是在還沒有 CORS 的時代,大家是怎么解決跨域的呢?答案就是 JSONP。
它的原理也非常簡單:雖然瀏覽器限制了 HTTP 的跨域,但是沒有限制獲取 script 標簽內容的跨域請求呀。
當我們插入一個 <script src="xxx.com"> 標簽的時候,會發一個獲取 xxx.com 的 GET 請求,而這個 GET 請求又不存在“跨域”限制,通過這樣的方法就能解決跨域的問題了。
服務端實現:
- router.get('/', (req, res) => {
- const { callback_name } = req.query;
- res.send(`${callback_name}('hello')`) // 返回 JS 代碼,調用 callback_name 這個函數,并傳入 hello
- });
前端實現:
- function jsonpCallback(params) {
- alert('執行 public/index.html 里定義的 jsonpCallback 函數,并傳入' + params + '參數');
- }
- const jsonp = async () => {
- // 制作 script 標簽
- const script = document.createElement('script');
- script.type = 'text/javascript';
- script.src = 'http://localhost:9000/user?callback_name=jsonpCallback'
- // 添加標簽
- document.body.appendChild(script);
- // 拿到數據再移除
- document.body.removeChild(script);
- }
- 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 中間件來配置,就不用手寫返回頭里的字段了。