1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        后端工程師的「跨域」之旅

        共 9720字,需瀏覽 20分鐘

         ·

        2022-01-22 19:17

        大家好,我是雷小帥!

        跨域,對后端工程師來說,可謂既熟悉又陌生。

        這兩個(gè)月我以架構(gòu)師的角色參與一款教育產(chǎn)品的孵化,有了一段難忘的跨域之旅

        寫這篇文章,我想分享我在跨域這個(gè)知識點(diǎn)上的經(jīng)歷和思考,希望對大家有所啟發(fā)。

        1 遇見跨域

        產(chǎn)品有多端:機(jī)構(gòu)端,局方端 ,家長端等 。每端都有獨(dú)立的域名,有的是在PC上訪問,有的是通過微信公眾號來訪問,有的是掃碼后H5展現(xiàn)。

        接入層調(diào)用的接口域名統(tǒng)一使用 api.training.com這個(gè)獨(dú)立的域名,通過Nginx來配置請求轉(zhuǎn)發(fā)。

        通常,我們提到的跨域指:CORS。

        CORS是一個(gè)W3C標(biāo)準(zhǔn),全稱是"跨域資源共享"(Cross-origin ?resource ?sharing), 它需要瀏覽器和服務(wù)器同時(shí)支持他,允許瀏覽器向跨源服務(wù)器發(fā)送XMLHttpRequest請求,從而克服 AJAX 只能同源使用的限制。

        那么如何定義同源呢?我們先看下一個(gè)典型的網(wǎng)站的地址:

        同源是指:協(xié)議、域名、端口號完全相同。

        下表給出了與 URL http://www.training.com/dir/page.html 的源進(jìn)行對比的示例:

        當(dāng)用戶通過瀏覽器訪問應(yīng)用(http://admin.training.com)時(shí),調(diào)用接口的域名非同源域名(http://api.training.com),這是顯而易見的跨域場景。

        2 ?CORS詳解

        跨域資源共享標(biāo)準(zhǔn)新增了一組 HTTP 首部字段,允許服務(wù)器聲明哪些源站通過瀏覽器有權(quán)限訪問哪些資源。

        規(guī)范要求,對那些可能對服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發(fā)起一個(gè)預(yù)檢請求(preflight request),從而獲知服務(wù)端是否允許該跨域請求。

        服務(wù)器確認(rèn)允許之后,才發(fā)起實(shí)際的 HTTP 請求。在預(yù)檢請求的返回中,服務(wù)器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認(rèn)證相關(guān)數(shù)據(jù))。

        2.1 簡單請求

        當(dāng)請求同時(shí)滿足如下條件時(shí),CORS驗(yàn)證機(jī)制會(huì)使用簡單請求, 否則CORS驗(yàn)證機(jī)制會(huì)使用預(yù)檢請求。

        1. 使用GET、POST、HEAD其中一種方法;
        2. 只使用了如下的安全首部字段,不得人為設(shè)置其他首部字段;
          • Accept
          • Accept-Language
          • Content-Language
          • Content-Type 僅限三種之一:text/plain,multipart/form-data,application/x-www-form-urlencoded:
          • HTML頭部 header field字段:DPR、Download、Save-Data、Viewport-Width、WIdth
        3. 請求中的任意 XMLHttpRequestUpload ?對象均沒有注冊任何事件監(jiān)聽器;XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問;
        4. 請求中沒有使用 ReadableStream 對象。

        簡單請求模式,瀏覽器直接發(fā)送跨域請求,并在請求頭中攜帶Origin的頭,表明這是一個(gè)跨域的請求。服務(wù)器端接到請求后,會(huì)根據(jù)自己的跨域規(guī)則,通過Access-Control-Allow-Origin和Access-Control-Allow-Methods響應(yīng)頭,來返回驗(yàn)證結(jié)果。

        應(yīng)答中攜帶了跨域頭 Access-Control-Allow-Origin。使用 Origin 和 Access-Control-Allow-Origin 就能完成最簡單的訪問控制。本例中,服務(wù)端返回的 Access-Control-Allow-Origin: * 表明,該資源可以被任意外域訪問。如果服務(wù)端僅允許來自 http://admin.training.com 的訪問,該首部字段的內(nèi)容如下:

        Access-Control-Allow-Origin:?http://admin.training.com

        現(xiàn)在,除了 http://admin.training.com,其它外域均不能訪問該資源。

        2.2 預(yù)檢請求

        瀏覽器在發(fā)現(xiàn)頁面發(fā)出的請求非簡單請求,并不會(huì)立即執(zhí)行對應(yīng)的請求代碼,而是會(huì)觸發(fā)預(yù)先請求模式。預(yù)先請求模式會(huì)先發(fā)送preflight request(預(yù)先驗(yàn)證請求),preflight request是一個(gè)OPTION請求,用于詢問要被跨域訪問的服務(wù)器,是否允許當(dāng)前域名下的頁面發(fā)送跨域的請求。在得到服務(wù)器的跨域授權(quán)后才能發(fā)送真正的HTTP請求。

        OPTIONS請求頭部中會(huì)包含以下頭部:

        服務(wù)器收到OPTIONS請求后,設(shè)置頭部與瀏覽器溝通來判斷是否允許這個(gè)請求。

        如果preflight request驗(yàn)證通過,瀏覽器才會(huì)發(fā)送真正的跨域請求。

        3 ?后端配置

        后端配置我嘗試過兩種方式,經(jīng)過兩個(gè)月的測試,都能非常穩(wěn)定的運(yùn)行。

        • MND推薦的Nginx配置;
        • SpringBoot自帶CorsFilter配置。

        ▍MND推薦的Nginx配置

        Nginx配置相當(dāng)于在請求轉(zhuǎn)發(fā)層配置。

        location?/?{
        ?????if?($request_method?=?'OPTIONS')?{
        ????????add_header?'Access-Control-Allow-Origin'?'*';
        ????????add_header?'Access-Control-Allow-Methods'?'GET,?POST,?OPTIONS';
        ????????#
        ????????#?Custom?headers?and?headers?various?browsers?*should*?be?OK?with?but?aren't
        ????????#
        ????????add_header?'Access-Control-Allow-Headers'?'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        ????????#
        ????????#?Tell?client?that?this?pre-flight?info?is?valid?for?20?days
        ????????#
        ????????add_header?'Access-Control-Max-Age'?1728000;
        ????????add_header?'Content-Type'?'text/plain;?charset=utf-8';
        ????????add_header?'Content-Length'?0;
        ????????return?204;
        ?????}
        ?????if?($request_method?=?'POST')?{
        ????????add_header?'Access-Control-Allow-Origin'?'*'?always;
        ????????add_header?'Access-Control-Allow-Methods'?'GET,?POST,?OPTIONS'?always;
        ????????add_header?'Access-Control-Allow-Headers'?'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'?always;
        ????????add_header?'Access-Control-Expose-Headers'?'Content-Length,Content-Range'?always;
        ?????}
        ?????if?($request_method?=?'GET')?{
        ????????add_header?'Access-Control-Allow-Origin'?'*'?always;
        ????????add_header?'Access-Control-Allow-Methods'?'GET,?POST,?OPTIONS'?always;
        ????????add_header?'Access-Control-Allow-Headers'?'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'?always;
        ????????add_header?'Access-Control-Expose-Headers'?'Content-Length,Content-Range'?always;
        ?????}
        }

        在配置Access-Control-Allow-Headers屬性的時(shí)候,因?yàn)樽远x的header包含簽名和token,數(shù)量較多。為了簡潔方便,我把Access-Control-Allow-Headers配置成 * 。

        在Chrome和firefox下沒有任何異常,但在IE11下報(bào)了如下的錯(cuò):

        Access-Control-Allow-Headers 列表中不存在請求標(biāo)頭 content-type。

        原來IE11要求預(yù)檢請求返回的Access-Control-Allow-Headers的值必須以逗號分隔。

        ▍SpringBoot自帶CorsFilter

        首先基礎(chǔ)框架里默認(rèn)有如下跨域配置。

        public?void?addCorsMappings(CorsRegistry?registry)?{
        ????registry.addMapping("/**")
        ??????.allowedOrigins("*")
        ??????.allowedMethods("POST",?"GET",?"PUT",?"OPTIONS",?"DELETE")
        ??????.allowCredentials(true)
        ??????.allowedHeaders("*")
        ??????.maxAge(3600);
        }

        可是部署完成,進(jìn)入還是報(bào)CORS異常:

        從nginx和tomcat日志來看,僅僅收到一個(gè)OPTION請求,springboot應(yīng)用里有一個(gè)攔截器ActionInterceptor,從header中獲取token,調(diào)用用戶服務(wù)查詢用戶信息,放入request中。當(dāng)沒有獲取token數(shù)據(jù)時(shí),會(huì)返回給前端JSON格式數(shù)據(jù)。

        但從現(xiàn)象來看CorsMapping并沒有生效。

        為什么呢?實(shí)際上還是執(zhí)行順序的概念。下圖展示了 過濾器,攔截器,控制器的執(zhí)行順序。

        DispatchServlet.doDispatch()方法是SpringMVC的核心入口方法。

        //?Determine?handler?for?the?current?request.
        mappedHandler?=?getHandler(processedRequest);
        if?(!mappedHandler.applyPreHandle(processedRequest,?response))?{
        ????return;
        }
        //?Actually?invoke?the?handler.
        mv?=?ha.handle(processedRequest,?response,?mappedHandler.getHandler());

        那么CorsMapping在哪里初始化的呢?經(jīng)過調(diào)試,定位于AbstractHandlerMapping

        protected?HandlerExecutionChain?getCorsHandlerExecutionChain(HttpServletRequest?request,
        ??HandlerExecutionChain?chain,?CorsConfiguration?config)
        ?
        {
        ??if?(CorsUtils.isPreFlightRequest(request))?{
        ???HandlerInterceptor[]?interceptors?=?chain.getInterceptors();
        ???chain?=?new?HandlerExecutionChain(new?PreFlightHandler(config),?interceptors);
        ??}
        ??else?{
        ???chain.addInterceptor(new?CorsInterceptor(config));
        ???}
        ??return?chain;
        ?}

        代碼里有預(yù)檢判斷,通過 PreFlightHandler.handleRequest() 中處理,但是處于正常的業(yè)務(wù)攔截器之后。

        最終選擇CorsFilter 主要基于兩點(diǎn)原因:

        • 過濾器的執(zhí)行順序優(yōu)先級最高;
        • 通過調(diào)試CorsFilter的源碼,發(fā)現(xiàn)源碼有很多細(xì)節(jié)的處理。
        private?CorsConfiguration?corsConfig()?{
        ????CorsConfiguration?corsConfiguration?=?new?CorsConfiguration();
        ????corsConfiguration.addAllowedOrigin("*");
        ????corsConfiguration.addAllowedHeader("*");
        ????corsConfiguration.addAllowedMethod("*");
        ????corsConfiguration.setAllowCredentials(true);
        ????corsConfiguration.setMaxAge(3600L);
        ????return?corsConfiguration;
        }
        @Bean
        public?CorsFilter?corsFilter()?{
        ????UrlBasedCorsConfigurationSource?source?=?new?UrlBasedCorsConfigurationSource();
        ????source.registerCorsConfiguration("/**",?corsConfig());
        ????return?new?CorsFilter(source);
        }

        下面的代碼里,allowHeader是通配符 * 的時(shí)候,CorsFilter在設(shè)置 Access-Control-Allow-Headers 的時(shí)候,會(huì)將 Access-Control-Request-Headers 以逗號拼接起來,這樣就可以避免IE11響應(yīng)頭的問題。

        public?List?checkHeaders(@Nullable?List?requestHeaders)?{
        ???if?(requestHeaders?==?null)?{
        ??????return?null;
        ???}
        ???if?(requestHeaders.isEmpty())?{
        ??????return?Collections.emptyList();
        ???}
        ???if?(ObjectUtils.isEmpty(this.allowedHeaders))?{
        ??????return?null;
        ???}

        ???boolean?allowAnyHeader?=?this.allowedHeaders.contains(ALL);
        ???List?result?=?new?ArrayList<>(requestHeaders.size());
        ???for?(String?requestHeader?:?requestHeaders)?{
        ??????if?(StringUtils.hasText(requestHeader))?{
        ?????????requestHeader?=?requestHeader.trim();
        ?????????if?(allowAnyHeader)?{
        ????????????result.add(requestHeader);
        ?????????}
        ?????????else?{
        ????????????for?(String?allowedHeader?:?this.allowedHeaders)?{
        ???????????????if?(requestHeader.equalsIgnoreCase(allowedHeader))?{
        ??????????????????result.add(requestHeader);
        ??????????????????break;
        ???????????????}
        ????????????}
        ?????????}
        ??????}
        ???}
        ???return?(result.isEmpty()???null?:?result);
        }

        瀏覽器的執(zhí)行效果如下:

        4 ?preflight響應(yīng)碼:200 vs 204

        后端配置完成之后,團(tuán)隊(duì)里的小伙伴問我:“勇哥,那預(yù)檢請求返回的響應(yīng)碼到底是200還是204呀?”。這個(gè)問題真把我給問住了。

        我司的API網(wǎng)關(guān)的預(yù)檢響應(yīng)碼是200,CorsFilter預(yù)檢響應(yīng)碼也是200。

        MDN給的示例預(yù)檢響應(yīng)碼全部是204。

        https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

        我只能采取Google大法,赫然發(fā)現(xiàn)大名鼎鼎的API網(wǎng)關(guān)Kong的開發(fā)者也針對這個(gè)問題有一番討論。

        1. MDN曾經(jīng)推薦的preflight響應(yīng)碼是200 ,所以Kong也和MDN同步成200;

          The page was updated since then. See its contents on Sept 30th, 2018:

          https://web.archive.org/web/20180930031917/https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

        2. 后來MDN將響應(yīng)碼修改204,于是Kong的開發(fā)者爭論要不要和MDN保持同步。

          爭論的核心點(diǎn)在于:有沒有迫切的必要。200響應(yīng)碼運(yùn)行得很好,似乎也將永遠(yuǎn)正常運(yùn)行下去。而更換成204,不確定是否有隱藏問題。

        3. 說到底,框架開發(fā)者還是依賴于瀏覽器的底層實(shí)現(xiàn)。在這個(gè)問題上,沒有足夠權(quán)威的資料能夠支撐框架開發(fā)者,而各個(gè)知識點(diǎn)都散落在網(wǎng)絡(luò)的各個(gè)角落,充斥著不完整的細(xì)節(jié)和部分解決方案,這些都讓框架開發(fā)者非常困惑。

        最后,Kong的源碼里預(yù)檢響應(yīng)碼仍然是200,并沒有和MDN保持同步。

        我仔細(xì)查看了各大主流網(wǎng)站,95%預(yù)檢響應(yīng)碼是200。而經(jīng)過兩個(gè)多月的測試,Nginx配置預(yù)檢響應(yīng)碼204,在主流的瀏覽器Chrome , Firefox , ?IE11 也沒有出現(xiàn)任何問題。

        所以,200 works everywhere, 而204在當(dāng)前主流的瀏覽器里也得到非常好的支持。

        5 ?Chrome: 非安全私有網(wǎng)絡(luò)

        本以為跨域問題就這樣解決了。沒想到還是有一個(gè)小插曲。

        產(chǎn)品總監(jiān)需要給客戶做演示,我負(fù)責(zé)搞定演示環(huán)境。申請域名,準(zhǔn)備阿里云服務(wù)器,應(yīng)用打包,部署,一切都很順利。

        可是在公司內(nèi)網(wǎng)訪問演示環(huán)境,有一個(gè)頁面一直報(bào)CORS報(bào)錯(cuò),報(bào)錯(cuò)內(nèi)容類似下圖:

        跨域的錯(cuò)誤類型是:InsecurePrivateNetwork

        這和原來遇到的跨域錯(cuò)誤完全不一樣,我心里一慌。馬上Google , 原來這是chrome更新到94之后新的特性,可以手工關(guān)閉這個(gè)特性。

        1. 打開 tab 頁面 ?chrome://flags/#block-insecure-private-network-requests
        2. 將其 ?Block insecure private network requests 設(shè)置為 Disabled, 然后重啟就行了, 這樣子就相當(dāng)于把這個(gè)功能禁用掉。

        但這樣是治標(biāo)不治本呀。有點(diǎn)詭異的是,當(dāng)我們不在公司內(nèi)網(wǎng)訪問演示環(huán)境的時(shí)候,演示環(huán)境完全正常,出錯(cuò)的頁面也能正常訪問。

        仔細(xì)看官方的文檔,CORS-RFC1918 指出如下三種請求會(huì)受影響。

        • 公共網(wǎng)絡(luò)訪問私有網(wǎng)絡(luò);
        • 公共網(wǎng)絡(luò)訪問本地設(shè)備;
        • 私有網(wǎng)絡(luò)訪問本地設(shè)備。

        這樣,我把問題定位在這個(gè)出錯(cuò)的第三方接口地址上。公司很多產(chǎn)品都依賴這個(gè)接口服務(wù)。當(dāng)在公司內(nèi)網(wǎng)訪問的時(shí)候,該域名映射地址類似:172.16.xx.xx。

        而這個(gè)ip正好是rfc1918上規(guī)定的私有網(wǎng)絡(luò)。

        10.0.0.0?????-??10.255.255.255??(10/8?prefix)
        172.16.0.0???-??172.31.255.255??(172.16/12?prefix)
        192.168.0.0??-??192.168.255.255?(192.168/16?prefix)

        內(nèi)網(wǎng)通過Chrome訪問這個(gè)頁面的時(shí)候,會(huì)觸發(fā)非安全私有網(wǎng)絡(luò)攔截。

        如何解決呢?官方給出的方案分兩步走:

        1. 私有網(wǎng)絡(luò)只能通過Https來訪問;
        2. 未來,添加特定的預(yù)檢頭,比如說:Access-Control-Request-Private-Network等。

        當(dāng)然還有一些臨時(shí)方法:

        • 關(guān)閉Chrome該特性;
        • 換用其他瀏覽器比如Firefox;
        • 關(guān)閉網(wǎng)絡(luò)內(nèi)網(wǎng)開手機(jī)熱點(diǎn);
        • 修改本地host綁定外網(wǎng)ip。

        基于官方的方案 ,生產(chǎn)環(huán)境完全使用Https,公司內(nèi)網(wǎng)訪問就沒有出現(xiàn)這樣的跨域問題了。

        6 復(fù)盤

        美團(tuán)Shepherd API網(wǎng)關(guān)的整體架構(gòu)

        API網(wǎng)關(guān)非常適合當(dāng)前產(chǎn)品的架構(gòu)。架構(gòu)設(shè)計(jì)之初,系統(tǒng)多端都會(huì)調(diào)用我司的API網(wǎng)關(guān)。API網(wǎng)關(guān)可以SAAS部署和私有化部署,有單獨(dú)的域名,提供完善的簽名算法。考慮到上線時(shí)間節(jié)點(diǎn),團(tuán)隊(duì)成員對于API網(wǎng)關(guān)的熟悉程度以及多套環(huán)境部署投入時(shí)間成本,為了盡快交付,從架構(gòu)層面,我做了一些平衡和妥協(xié)。

        接入層調(diào)用的接口域名統(tǒng)一使用 api.training.com這個(gè)獨(dú)立的域名,通過Nginx來配置請求轉(zhuǎn)發(fā)。同時(shí),我和前端Leader統(tǒng)一了前后端協(xié)議,保持和我司API網(wǎng)關(guān)一致,為后續(xù)切回API網(wǎng)關(guān)做前置準(zhǔn)備。

        API網(wǎng)關(guān)可以做鑒權(quán),限流,灰度等,同時(shí)可以配置CORS。內(nèi)部服務(wù)端不用特別關(guān)注跨域這個(gè)問題。

        騰訊API網(wǎng)關(guān)的配置界面

        同時(shí),在解決跨域的問題過程中,我的心態(tài)也發(fā)生了變化。從最初的輕視,到逐漸沉下心來,一步步理解CORS的原理,分清楚不同解決方案的優(yōu)缺點(diǎn),事情也就慢慢順?biāo)炱饋?。我也觀察到:”有的項(xiàng)目組已經(jīng)反饋過Chrome非安全私有網(wǎng)絡(luò)問題,并給出了解決方案。對于技術(shù)管理者來講,一定要重視項(xiàng)目中反饋的問題,做好梳理分析,整理預(yù)案。這樣當(dāng)同類問題出現(xiàn)時(shí),也會(huì)條理有序“。

        7 ?寫到最后

        2016年,我參加左耳朵耗子陳皓老師技術(shù)演講,他給我們講了一個(gè)故事。

        故事的大概是:“公司軟件出現(xiàn)莫名BUG,用戶的費(fèi)用扣了,但調(diào)用第三方接口的時(shí)候經(jīng)常出現(xiàn)網(wǎng)絡(luò)問題。公司當(dāng)時(shí)最厲害的人查了一周也沒有解決,而陳皓老師正在看《TCP/IP 詳解》這本書, netstat 一看,連接的狀態(tài)是 CLOSE_WAIT ,意思是對方斷開了連接,大概率估計(jì)是對方系統(tǒng)的問題。于是他去了對方那邊幫他們看了一下代碼,果然是判斷條件出了問題,導(dǎo)致應(yīng)用直接斷開了鏈接。而這個(gè)問題只花了不到兩個(gè)小時(shí)就解決了”。

        當(dāng)我想起陳皓老師的這個(gè)故事,回顧自己的跨域之旅,我深深的覺得細(xì)節(jié)是魔鬼,而解決問題也許就在某個(gè)不經(jīng)意的細(xì)節(jié)里。


        如果我的文章對你有所幫助,還請幫忙點(diǎn)贊、在看、轉(zhuǎn)發(fā)一下,你的支持會(huì)激勵(lì)我輸出更高質(zhì)量的文章,非常感謝!

        瀏覽 34
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            美女小骚逼 | 无码影视在线观看 | 五月天乱伦视频 | 欧美日韩国产图片 | 欧美日韩一卡二卡 | 一区二区性爱视频 | 黄色免费网站入口 | 精品一区二区在线播放 | 国产精品久久99 | 国产黄片免费视频 |