Cors跨域(一):深入理解跨域請求概念及其根因
前言
你好,我是YourBatman。
做Web開發(fā)的小伙伴對“跨域”定并不陌生,像狗皮膏藥一樣粘著幾乎每位同學(xué),對它可謂既愛又恨??缬蛘埱笾趧?chuàng)業(yè)、小型公司來講是個頭疼的問題,因為這類企業(yè)還未沉淀出一套行之有效的、統(tǒng)一的解決方案。
讓人擔(dān)憂的是,據(jù)我了解不少程序員同學(xué)(不乏有高級開發(fā))碰到跨域問題大都一頭霧水:
然后很自然的 用谷歌去百度一下搜索答案,但相關(guān)文章可能參差不齊、魚龍混雜。短則半天長則一天(包含改代碼、部署等流程)此問題才得以解決,一個“小小跨域”問題成功偷走你的寶貴時間。
既然跨域是個如此常見(特別是當(dāng)下前后端分離的開發(fā)模式),因此深入理解CORS變得就異常的重要了(反倒前端工程師不用太了解),因此早在2019年我剛開始寫博客那會就有過較為詳細的系列文章:
現(xiàn)在把它搬到公眾號形成技術(shù)專欄,并且加點料,讓它更深、更全面、更系統(tǒng)的幫助到你,希望可以助你從此不再怕Cors跨域資源共享問題。
本文提綱
版本約定
- JDK:8
- Servlet:4.x
正文
文章遵循一貫的風(fēng)格,本文將采用概念 + 代碼示例的方式,層層遞進的進行展開敘述。那么上菜,先來個示例預(yù)覽,模擬一下跨域請求,后面的一些的概念示例將以此作為抓手。
模擬跨域請求
要模擬跨域請求的根本是需要兩個源:讓請求的來源和目標(biāo)源不一樣。這里我就使用IDEA作為靜態(tài)Web服務(wù)器(63342),Tomcat作為后端動態(tài)Servlet服務(wù)器(8080)。
❝說明:服務(wù)器都在本機,端口不一樣即可❞
前端代碼
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>CORS跨域請求</title>
- <!--導(dǎo)入Jquery-->
- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
- </head>
- <body>
- <button id="btn">跨域從服務(wù)端獲取內(nèi)容</button>
- <div id="content"></div>
- <script>
- $("#btn").click(function () {
- // 跨域請求
- $.get("http://localhost:8080/cors", function (result) {
- $("#content").append(result).append("<br/>");
- });
- // 同域請求
- $.get("http://localhost:63342");
- $.post("http://localhost:63342");
- });
- </script>
- </body>
- </html>
使用IDEA作為靜態(tài)web服務(wù)器,瀏覽器輸入地址即可訪問(注:端口號為63342):
后端代碼
后端寫個Servlet來接收cors請求
- /**
- * 在此處添加備注信息
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
- * @site https://yourbatman.cn
- * @date 2021/6/9 10:36
- * @since 0.0.1
- */
- @Slf4j
- @WebServlet(urlPatterns = "/cors")
- public class CorsServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- String requestURI = req.getRequestURI();
- String method = req.getMethod();
- String originHeader = req.getHeader("Origin");
- log.info("收到請求:{},方法:{}, Origin頭:{}", requestURI, method, originHeader);
- resp.getWriter().write("hello cors...");
- }
- }
啟動后端服務(wù)器,點擊頁面上的按鈕,結(jié)果如下:
服務(wù)端控制臺輸出:
- ... INFO c.y.cors.servlet.CorsServlet - 收到請求:/cors,方法:GET, Origin頭:http://localhost:63342
❝服務(wù)端輸出日志,說明即使前端的Http Status是error,但服務(wù)端還是收到并處理了這個請求的❞下面以此代碼示例為基礎(chǔ),普及一下和Cors跨域資源共享相關(guān)的概念。
Host、Referer、Origin的區(qū)別
這哥三看起來很是相似,下面對概念作出區(qū)分。
1.Host:去哪里。域名+端口。值為客戶端將要訪問的遠程主機,瀏覽器在發(fā)送Http請求時會帶有此Header
2.Referer:來自哪里。協(xié)議+域名+端口+路徑+參數(shù)。當(dāng)前請求的來源頁面的地址,服務(wù)端一般使用 Referer 首部識別訪問來源,可能會以此進行統(tǒng)計分析、日志記錄以及緩存優(yōu)化等
- 來源頁面協(xié)議為File或者Data URI(如頁面從本地打開的)
- 來源頁面是Https,而目標(biāo)URL是http
- 瀏覽器地址欄直接輸入網(wǎng)址訪問,或者通過瀏覽器的書簽直接訪問
- 使用JS的location.href跳轉(zhuǎn)
- ...
常見應(yīng)用場景:百度的搜索廣告就會分析Referer來判斷打開站點是從百度搜索跳轉(zhuǎn)的,還是直接URL輸入地址的
一般情況下瀏覽器會帶有此Header,但這些case不會帶有Referer這個頭
3.Origin:來自哪里(跨域)。協(xié)議+域名+端口。它用于Cors請求和同域POST請求
可以看到Referer與Origin功能相似,前者一般用于統(tǒng)計和阻止盜鏈,后者用于CORS請求。但是還是有幾點不同:
1.只有跨域請求,或者同域時發(fā)送post請求,才會攜帶Origin請求頭;而Referer只要瀏覽器能獲取到都會攜帶(除了上面說明的幾種case外)
2.若瀏覽器不能獲取到請求源頁面地址(如上面的幾種case),Referer頭不會發(fā)送,但Origin依舊會發(fā)送,只是值是null而已(注:雖然值為null,但此請求依舊屬于Cors請求哦),如下圖所示:
3.Origin的值只包括協(xié)議、域名和端口,而Rerferer不但包括協(xié)議、域名、端口還包括路徑,參數(shù),注意不包括hash值
瀏覽器的同源策略
瀏覽器的職責(zé)是展示/渲染document、css、script腳本等,但是這些資源(將document、css、script統(tǒng)一稱為資源)可能來自不同的地方,如本地、遠程服務(wù)器、甚至黑客的服務(wù)器......瀏覽器作為萬維網(wǎng)的入口,是我們接入互聯(lián)網(wǎng)最重要的軟件之一(甚至沒有之一),因此它的安全性顯得尤為重要,這就出現(xiàn)了瀏覽器的同源策略。
同源策略是瀏覽器一個重要的安全策略,它用于限制一個origin源的document或者它加載的腳本如何能與另一個origin源的資源進行交互。它能幫助阻隔惡意文檔,減少(并不是杜絕)可能被攻擊的媒介。
❝方便和安全往往是相悖的:安全性增高了,方便性就會有所降低❞那么問題來了,什么才算同源?
同源的定義
URL被稱作:統(tǒng)一資源定位符,同源是針對URL而言的。一個完整的URL各部分如下圖所示:
❝Tips:域名和host是等同的概念,域名+端口號 = host+端口號(大部分情況下你看到域名并沒有端口號,那是采用了默認(rèn)端口號80而已)❞同源:只和上圖的前兩部分(protocol + domain)有關(guān),規(guī)則為:全部相同則為同源。這個定義不難理解,但有幾點需要再強調(diào)一下:
- 兩部分必須完全一樣才算同源
- 這里的domain包含port端口號,所以總共是兩部分而非三部分
當(dāng)然也有說三部分的(協(xié)議+host+port),理解其含義就成
下面通過舉例來徹底了解下。譬如,我的源URL為:http://www.baidu.com/api/user,下面表格描述了不同URL的各類情況:
不同源的網(wǎng)絡(luò)訪問
瀏覽器同源策略的存在,限制了不同源之間的交互,實為不便。但是瀏覽器也開了一些“綠燈”,讓其不受同源策略的約束。此種情況一般可分為如下三類:
1.跨域?qū)懖僮?Cross-origin writes):一般是被允許的。如鏈接(如a標(biāo)簽)、重定向以及表單提交(如form表單的提交)
2.跨域資源嵌入(Cross-origin embedding):一般是允許的。比如下面這些例子:
- <script src="..."></script>標(biāo)簽嵌入js腳本
- <link rel="stylesheet" href="...">標(biāo)簽嵌入CSS
- <img>展示的圖片
- <video>和<audio>媒體資源
- <object>、 <embed> 、<applet>嵌入的插件
- CSS中使用@font-face引入字體
- 通過<iframe>載入資源
3.跨域讀操作(Cross-origin reads):一般是不被允許的。比如我們的http接口請求等都屬于此范疇,也是本專欄關(guān)注的焦點
簡單總結(jié)成一句話:瀏覽器自己是可以發(fā)起跨域請求的(比如a標(biāo)簽、img標(biāo)簽、form表單等),但是Javascript是不能去跨域獲取資源(如ajax)。
如何允許不同源的網(wǎng)絡(luò)訪問
上面說到的第三種情況:跨域讀操作一般是不允許跨域訪問的,而這種情況是我們開發(fā)過程中最關(guān)心、最常見的case,因此必須解決。
❝Tips:這里的讀指的是廣義上的讀,指的是從服務(wù)器獲取資源(有response)的都叫讀操作,而和具體是什么Http Method無關(guān)。換句話講,所有的Http API接口請求都在這里都指的是讀操作❞可以使用 CORS 來允許跨源訪問。CORS 是 HTTP 的一部分,它允許服務(wù)端來指定哪些主機可以從這個服務(wù)端加載資源。
什么是Cors跨域
Cors(Cross-origin resource sharing):跨域資源共享,它是瀏覽器的一個技術(shù)規(guī)范,由W3C規(guī)定,規(guī)范的wiki地址在此:https://www.w3.org/wiki/CORS_Enabled#What_is_CORS_about.3F
❝話外音:它是瀏覽器的一種(自我保護)行為,并且已形成規(guī)范。也就是說:backend請求backend是不存在此現(xiàn)象的嘍❞若想實現(xiàn)Cors機制的跨域請求,是需要瀏覽器和服務(wù)器同時支持的。關(guān)于瀏覽器對CORS的支持情況:現(xiàn)在都2021年了,so可以認(rèn)為100%的瀏覽器都是支持的,再加上CORS的整個過程都由瀏覽器自動完成,前端無需做任何設(shè)置,所以前端工程師的ajax原來怎么用現(xiàn)在還是怎么用,它對前段開發(fā)人員是完全透明的。
為何需要Cors跨域訪問?
瀏覽器費盡心思的搞個同源策略來保護我們的安全,但為何又需要跨域來打破這種安全策略呢?其實啊,這一切都和互聯(lián)網(wǎng)的快速發(fā)展有關(guān)~
隨著Web開放的程度越來越高,頁面的內(nèi)容也是越來越豐富。因此頁面上出現(xiàn)的元素也就越來越多:圖片、視頻、各種文字內(nèi)容等。為了分而治之,一個頁面的內(nèi)容可能來自不同地方,也就是不同的domain域,因此通過API跨域訪問成了必然。
瀏覽器作為進入Internet最大的入口,很長時間它是個大互聯(lián)公司的必爭之地,因此市面上并存的瀏覽器種類繁多且魚龍混扎:IE 7、8、9、10,Chrome、Safari、火狐,每個瀏覽器對跨域的實現(xiàn)可能都不一樣。因此對開發(fā)者而言亟待需要一個規(guī)范的、統(tǒng)一方案,它就是Cors。
CORS(Cross-Origin Resource Sharing)由W3C組織于2009-03-17編寫工作草案,直到2014-01-16才正式畢業(yè)成為行業(yè)規(guī)范,所有瀏覽器得以遵守。至此,程序員同學(xué)們在解決跨域問題上,只需按照Cors規(guī)范實施即可。
Cors的工作原理
Web資源涉及到兩個角色:瀏覽器(消費者)和服務(wù)器(提供者),面向這兩個角色來了解Cors的原理非常簡單,如下圖所示:
1.若瀏覽器發(fā)送的是個跨域請求,http請求中就會攜帶一個名為Origin的頭表明自己的“位置”,如Origin: http://localhost:5432
2.服務(wù)端接到請求后,就可以根據(jù)傳過來的Origin頭做邏輯,決定是否要將資源共享給這個源嘍。而這個決定通過響應(yīng)頭Access-Control-Allow-Origin來承載,它的value值可以是任意值,有如下情況:
- 值為*,通配符,允許所有的Origin共享此資源
- 值為http://localhost:5432(也就是和Origin相同),共享給此Origin
- 值為非http://localhost:5432(也就是和Origin不相同),不共享給此Origin
- 無此頭:不共享給此origin
- 有此頭:值有如下可能情況
3.瀏覽器接收到Response響應(yīng)后,會去提取Access-Control-Allow-Origin這個頭。然后根據(jù)上述規(guī)則來決定要接收此響應(yīng)內(nèi)容還是拒絕
❝Tips:Access-Control-Allow-Origin響應(yīng)頭只能有1個,且value值就是個字符串。另外,value值即使寫為http://aa.com,http://bb.com這種也屬于一個而非兩個值❞
Cors細粒度控制:授權(quán)響應(yīng)頭
在Cors規(guī)范中,除了可以通過Access-Control-Allow-Origin響應(yīng)頭來對主體資源(URL級別)進行授權(quán)外,還提供了針對于具體響應(yīng)頭更細粒度的控制,這個響應(yīng)頭就是:Access-Control-Expose-Headers。換句話講,該頭用于規(guī)定哪些響應(yīng)頭(們)可以暴露給前端,默認(rèn)情況下這6個響應(yīng)頭無需特別的顯示指定就支持:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
若不在此值里面的頭將不會返回給前端(其實返回了,只是瀏覽器讓其對前端不可見了而已,對JavaScript也不可見哦)。
但是,但是,但是,這種細粒度控制header的機制對簡單請求是無效的,只針對于非簡單請求(也叫復(fù)雜請求)。由此可見,將哪些類型的跨域資源請求劃分為簡單請求的范疇就顯得特備重要了。
何為簡單請求
Cors規(guī)范定義簡單請求的原則是:請求不是以更新(添加、修改和刪除)資源為目的,服務(wù)端對請求的處理不會導(dǎo)致自身維護資源的改變。對于簡單跨域資源請求來說,瀏覽器將兩個步驟(取得授權(quán)和獲取資源)合二為一,由于不涉及到資源的改變,所以不會帶來任何副作用。
對于一個請求,必須同時符合如下要求才被劃為簡單請求:
1.Http Method只能為其一:
- GET
- POST
- HEAD
2.請求頭只能在如下范圍:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- Accept
- Accept-Language
- Content-Language
- Content-Type,其中它的值必須如下其一:
除此之外的請求都為非簡單請求(也可稱為復(fù)雜請求)。非簡單請求可能對服務(wù)端資源改變,因此Cors規(guī)定瀏覽器在發(fā)出此類請求之前必須有一個“預(yù)檢(Preflight)”機制,這也就是我們熟悉的OPTIONS請求。
什么是Preflight預(yù)檢機制
顧名思義,它表示在瀏覽器發(fā)出真正請求之前,先發(fā)送一個預(yù)檢請求,這個在Http里就是OPTIONS請求方式。這個請求很特殊,它不包含主體(無請求參數(shù)、請求體等),主要就是將一些憑證、授權(quán)相關(guān)的輔助信息放在請求頭里交給服務(wù)器去做決策。因此它除了攜帶Origin請求頭外,還會額外攜帶如下兩個請求頭:
- Access-Control-Request-Method:真正請求的方法
- Access-Control-Request-Headers:真正請求的自定義請求頭(若沒有自定義的就是空唄)
服務(wù)端在接收到此類請求后,就可以根據(jù)其值做邏輯決策啦。如果允許預(yù)檢請求通過,返回個200即可,否則返回400或者403唄。
如果預(yù)檢成功,在響應(yīng)里應(yīng)該包含上文提到的響應(yīng)頭Access-Control-Allow-Origin和Access-Control-Expose-Headers,除此之外,服務(wù)端還可以做更精細化的控制,這些精細化控制的響應(yīng)頭為:
- Access-Control-Allow-Methods:允許實際請求的Http方法(們)
- Access-Control-Allow-Headers:允許實際請求的請求頭(們)
- Access-Control-Max-Age:允許瀏覽器緩存此結(jié)果多久,單位:秒。有了緩存,以后就不用每次請求都發(fā)送預(yù)檢請求啦
❝說明:以上響應(yīng)頭并不是必須的。若沒有此響應(yīng)頭,代表接受所有❞
預(yù)檢請求完成后,有個關(guān)鍵點,便是瀏覽器拿到預(yù)檢請求的響應(yīng)后的處理邏輯,這里描述如下:
1.先通過自己的Origin匹配預(yù)檢響應(yīng)中的Access-Control-Allow-Origin的值,若不匹配就結(jié)束請求,若匹配就繼續(xù)下一步驗證
- 關(guān)于Access-Control-Allow-Origin的驗證邏輯,請參考文上描述
2.拿到預(yù)檢響應(yīng)中的Access-Control-Allow-Methods頭。若此頭不存在,則進行下一步,若存在則校驗預(yù)檢請求頭Access-Control-Request-Method的值是否在此列表中,在其內(nèi)繼續(xù)下一步,否則失敗
3.拿到預(yù)檢響應(yīng)中的Access-Control-Request-Headers頭。同請求頭中的Access-Control-Allow-Headers值記性比較,全部包含在內(nèi)則匹配成功,否則失敗
以上全部匹配成功,就代表預(yù)檢成功,可以開始發(fā)送正式請求了。值得一提的事,Access-Control-Max-Age控制預(yù)檢結(jié)果的瀏覽器緩存,若緩存還生效的話,是不用單獨再發(fā)送OPTIONS請求的,匹配成功直接發(fā)送目標(biāo)真實即可。
Access-Control-Max-Age使用細節(jié)
Access-Control-Max-Age用于控制瀏覽器緩存預(yù)檢請求結(jié)果的時間,這里存在一些使用細節(jié)你需要注意:
1.若瀏覽器禁用了緩存,也就是勾選了Disable cache,那么此屬性無效。也就說每次都還得發(fā)送OPTIONS請求
2.判斷此緩存結(jié)果的因素有兩個:
- 必須是同一URL(也就是Origin相同才會去找對應(yīng)的緩存)
- header變化了,也會重新去發(fā)OPTIONS請求(當(dāng)然若去掉一些header編程簡單請求了,就另當(dāng)別論嘍)
跨域請求代碼示例
正所謂說再多,也抵不上跑幾個case,畢竟show me your code才是最重要。下面就針對跨域情況的簡單請求、非簡單請求(預(yù)檢通過、預(yù)檢不通過)等case分別用代碼(基于文首代碼)說明。
簡單請求
簡單請求正如其名,是最簡單的請求方式。
- // 跨域請求
- $.get("http://localhost:8080/cors", function (result) {
- $("#content").append(result).append("<br/>");
- });
服務(wù)端結(jié)果:
- INFO ...CorsServlet - 收到請求:/cors,方法:GET, Origin頭:http://localhost:63342
瀏覽器結(jié)果:
若想讓請求正常,只需在服務(wù)端響應(yīng)頭里“加點料”就成:
- ...
- resp.setHeader("Access-Control-Allow-Origin","http://localhost:63342");
- resp.getWriter().write("hello cors...");
- ...
再次請求,結(jié)果成功:
對于簡單請求來講,服務(wù)端只需要設(shè)置Access-Control-Allow-Origin這個一個頭即可,一個即可。
非簡單請求
非簡單請求的模擬非常簡單,隨便打破一個簡單請求的約束即可。比如我們先在上面get請求的基礎(chǔ)上自定義個請求頭:
- $.ajax({
- type: "get",
- url: "http://localhost:8080/cors",
- headers: {secret:"kkjtjnbgjlfrfgv",token: "abc123"}
- });
服務(wù)端代碼:
- /**
- * 在此處添加備注信息
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
- * @site https://yourbatman.cn
- * @date 2021/6/9 10:36
- * @since 0.0.1
- */
- @Slf4j
- @WebServlet(urlPatterns = "/cors")
- public class CorsServlet extends HttpServlet {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- String requestURI = req.getRequestURI();
- String method = req.getMethod();
- String originHeader = req.getHeader("Origin");
- log.info("收到請求:{},方法:{}, Origin頭:{}", requestURI, method, originHeader);
- resp.setHeader("Access-Control-Allow-Origin","http://localhost:63342");
- resp.setHeader("Access-Control-Expose-Headers","token,secret");
- resp.setHeader("Access-Control-Allow-Headers","token,secret"); // 一般來講,讓此頭的值是上面那個的【子集】(或相同)
- resp.getWriter().write("hello cors...");
- }
- }
點擊按鈕,瀏覽器發(fā)送請求,結(jié)果為:
服務(wù)端沒有任何日志輸出,也就是說瀏覽器并未把實際請求發(fā)出去。什么原因?查看OPTIONS請求的返回一看便知:
根本原因為:OPTIONS的響應(yīng)頭里并未含有任何跨域相關(guān)信息,雖然預(yù)檢通過(注意:這個預(yù)檢是通過的喲,預(yù)檢不通過的場景就不用額外演示了吧~),但預(yù)檢的結(jié)果經(jīng)瀏覽器判斷此跨域?qū)嶋H請求不能發(fā)出,所以給攔下來了。
從代碼層面問題就出現(xiàn)在resp.setHeader(xxx,xxx)放在了處理實際方法的Get方法上,顯然不對嘛,應(yīng)該放在doOptions()方法里才行:
- @Override
- protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- super.doOptions(req, resp);
- resp.setHeader("Access-Control-Allow-Origin","http://localhost:63342");
- resp.setHeader("Access-Control-Expose-Headers","token,secret");
- resp.setHeader("Access-Control-Allow-Headers","token,secret"); // 一般來講,讓此頭的值是上面那個的【子集】(或相同)
- }
在此運行,一切正常:
值得特別注意的是:設(shè)置跨域的響應(yīng)頭這塊代碼,在處理真實請求的doGet里也必須得有,否則服務(wù)端處理了,瀏覽器“不認(rèn)”也是會出跨域錯誤的。
另外就是,Access-Control-Allow-Headers/Access-Control-Expose-Headers這兩個頭里必須包含你的請求的自定義的Header(標(biāo)準(zhǔn)的header不需要包含),否則依舊跨域失敗哦~
在實際生產(chǎn)場景中,Http請求的Content-type大都是application/json并非簡單請求的頭,所以有個現(xiàn)實情況是:實際的跨域請求中,幾乎100%的情況下我們發(fā)的都是非簡單請求。
Cros跨域使用展望
如上代碼示例,處理簡單請求尚且簡單,但對于非簡單請求來說,我們在doOptions和doGet都寫了一段setHeader的代碼,是否覺得麻煩呢?
另外,對于Access-Control-Allow-Origin若我需要允許多個源怎么辦呢?
❝Tips:Access-Control-Allow-Origin頭只允許一個,且Access-Control-Allow-Origin:a.com,b.com依舊算作一個源的,它沒有逗號分隔的“特性”。從命名的藝術(shù)你也可看出,它并非是xxx-Origins而是xxx-Origin❞既然實際場景中幾乎100%都是非簡單請求,那么對于控制非簡單請求的Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Max-Age這些都都改如何賦值?是否有最佳實踐?
現(xiàn)在我們大都在Spring Framework/Spring Boot場景下開發(fā)應(yīng)用,框架層面是否提供一些優(yōu)雅的解決方案?
作為一名后端開發(fā)工程師(編程語言不限),也許你從未處理過跨域問題,那么到底是誰默默的幫你解決了這一切呢?是否想知其所以然?
總結(jié)
本文用很長的篇幅介紹了Cors跨域資源共享的相關(guān)知識,并且用代碼做了示范,希望能助你通關(guān)Cors這個狗皮膏藥一樣粘著我們的硬核知識點。本文文字?jǐn)⑹鲚^多,介紹了同源、跨域、Cors的幾乎所有概念,雖然略顯難啃,但這些是指導(dǎo)我們實踐的說明書。
本文轉(zhuǎn)載自微信公眾號「 BAT的烏托邦」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 BAT的烏托邦公眾號。