<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文了解 CORS 跨域

          共 15473字,需瀏覽 31分鐘

           ·

          2024-03-27 17:30

          作為一個(gè) Web 開發(fā),一定不會(huì)對(duì)下面的跨域報(bào)錯(cuò)陌生。

          bc76136244fcf9a7843c9a1dc6c87c8d.webp

          當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域或端口請(qǐng)求一個(gè)資源時(shí),資源會(huì)發(fā)起一個(gè)跨域 HTTP 請(qǐng)求。例如站點(diǎn) http://www.aliyun.com 的某 HTML 頁面請(qǐng)求 http://www.alibaba.com/image.jpg。

          出于安全原因,瀏覽器限制從頁面腳本內(nèi)發(fā)起的跨域請(qǐng)求,有些瀏覽器不會(huì)限制跨域請(qǐng)求的發(fā)起,但是會(huì)將結(jié)果攔截。 這意味著使用這些 API 的 Web 應(yīng)用只能加載同一個(gè)域下的資源,除非使用 CORS 機(jī)制(Cross-Origin Resource Sharing 跨源資源共享)獲取目標(biāo)服務(wù)器的授權(quán)來解決這個(gè)問題。

          這也是本文將要探討的主要問題,需要額外強(qiáng)調(diào)的是,跨域問題產(chǎn)生的主體是“瀏覽器”,這也是為什么,當(dāng)我們使用 curl、postman、各種語言的 HTTP 客戶端等工具時(shí),從來沒有被跨域問題困擾過。

          什么是跨域

          http://www.aliyun.com 站點(diǎn)訪問 http://www.alibaba.com/image.jpg 很容易被判斷為一個(gè)跨域請(qǐng)求,因?yàn)橛蛎灰粯樱床呗栽敿?xì)描述如下:

          • 協(xié)議相同
          • 域名相同
          • 端口相同

          以下是跨域與同源的一些示例

          站點(diǎn) 資源訪問 跨域 or 同源
          http://www.aliyun.com http://www.aliyun.com/hello 同源
          http://www.aliyun.com http://aliyun.com/hello 跨域(域名不同,子域名和父域名也屬于不同域名)
          http://www.aliyun.com https://www.aliyun.com/hello 跨域(協(xié)議不同)
          http://www.aliyun.com https://www.aliyun.com:81/hello 跨域(端口不同)

          同源策略存在的原因是為了保護(hù)用戶的安全和隱私,防止惡意網(wǎng)站對(duì)其他網(wǎng)站進(jìn)行攻擊或?yàn)E用。如果沒有同源機(jī)制,以下一些常見的跨域攻擊方式將會(huì)讓網(wǎng)站維護(hù)者不堪其擾:

          1. CSRF(Cross-Site Request Forgery):攻擊者在惡意網(wǎng)站中放置一個(gè)含有惡意請(qǐng)求的頁面,并誘使用戶訪問該頁面。當(dāng)用戶在其他網(wǎng)站登錄時(shí),惡意請(qǐng)求會(huì)自動(dòng)發(fā)送給目標(biāo)網(wǎng)站,以偽裝成用戶的操作。這樣,攻擊者可以利用用戶已經(jīng)登錄的憑證進(jìn)行惡意操作,如修改密碼、發(fā)起交易等。
          2. XSS(Cross-Site Scripting):攻擊者在合法網(wǎng)站的輸入框或評(píng)論中注入惡意腳本代碼。當(dāng)用戶訪問包含惡意腳本的頁面時(shí),腳本會(huì)在用戶的瀏覽器中執(zhí)行。攻擊者可以利用這種方式竊取用戶的登錄憑證、敏感信息或執(zhí)行其他惡意操作。
          3. Clickjacking:攻擊者通過在一個(gè)網(wǎng)頁上覆蓋一個(gè)透明的、惡意的圖層,來欺騙用戶點(diǎn)擊看似無害的內(nèi)容,實(shí)際上是觸發(fā)了惡意操作,如轉(zhuǎn)賬或進(jìn)行其他敏感操作。

          解決跨域問題,常見的方案有:

          1. CORS(跨域資源共享):在服務(wù)器端設(shè)置響應(yīng)頭部,允許指定的域名訪問資源。
          2. JSONP(JSON with Padding):通過在頁面中動(dòng)態(tài)添加 <script> 元素,利用 script 標(biāo)簽的跨域特性來獲取數(shù)據(jù)。
          3. 代理服務(wù)器:在服務(wù)器端設(shè)置一個(gè)代理服務(wù)器,將請(qǐng)求代理轉(zhuǎn)發(fā)到目標(biāo)服務(wù)器,繞過瀏覽器的同源策略。

          本文將會(huì)主要介紹 CORS 跨域資源共享方案。

          CORS 跨域資源共享介紹

          CORS 被定義在 w3c 規(guī)范中:https://fetch.spec.whatwg.org/#http-cors-protocol,這里包含了最詳細(xì)也最官方的描述。它并不是一個(gè)框架或者工具,而是一種機(jī)制、契約,當(dāng)瀏覽器和后端服務(wù)同時(shí)遵守 CORS 規(guī)范時(shí),跨域訪問便成了可能。根據(jù)使用經(jīng)驗(yàn),我們將 CORS 的機(jī)制分成了兩種模式:簡(jiǎn)單請(qǐng)求模式和預(yù)檢請(qǐng)求模式。

          同時(shí)符合以下條件,就屬于簡(jiǎn)單請(qǐng)求模式:

          1. 使用以下 HTTP 方法之一:GET、POST、HEAD
          2. 除了簡(jiǎn)單請(qǐng)求頭之外(例如 content-type),不能包含自定義請(qǐng)求頭(例如通過 XMLHttpRequest.setRequestHeader 設(shè)置的請(qǐng)求頭)
          3. Content-Type 為 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 之一 (application/x-www-form-urlencoded 由于是表單格式,屬于簡(jiǎn)單請(qǐng)求,而 application/json 的請(qǐng)求體需要拆包,不屬于簡(jiǎn)單請(qǐng)求)

          對(duì)于不符合簡(jiǎn)單請(qǐng)求模式的請(qǐng)求,瀏覽器將會(huì)啟用預(yù)檢請(qǐng)求模式。

          簡(jiǎn)單請(qǐng)求模式

          瀏覽器在出現(xiàn)跨域請(qǐng)求時(shí),會(huì)自動(dòng)給請(qǐng)求攜帶 Origin 請(qǐng)求頭,以下圖為例,是 http://edasnext.aliyun.com 發(fā)往 http://edas.aliyun.com 的一個(gè)跨域請(qǐng)求

          aa1f070a3dda7fcbcad934bac98705f5.webp

          服務(wù)端如果要正常支持跨域請(qǐng)求,在判斷當(dāng)前請(qǐng)求為跨域請(qǐng)求時(shí),需要在響應(yīng)中攜帶 Access-Control-Allow-Origin、Access-Control-Allow-Methods 等相關(guān)的響應(yīng)頭。如果 edasnext.aliyun.com 該來源不在服務(wù)端的跨域配置列表中,則返回 403 拒絕該請(qǐng)求。瀏覽器會(huì)檢查 Access 相關(guān)的響應(yīng)頭,如果沒有攜帶,則會(huì)出現(xiàn)文章最開始的跨域報(bào)錯(cuò)。

          Access to XMLHttpRequest at 'http://edas.aliyun.com/testCors' from origin 'http://edasnext.aliyun.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

          簡(jiǎn)單請(qǐng)求模式-快速開始

          為了更加直觀理解 CORS 的簡(jiǎn)單請(qǐng)求模式,本節(jié)快速開始給出了一個(gè)由 springboot 構(gòu)建的 demo,它和大多數(shù)業(yè)務(wù)應(yīng)用的項(xiàng)目結(jié)構(gòu)類似。

          編寫 RestController

                
                @RestController
          public class IndexController {

              @RequestMapping("/testCors")
              public String testCors(HttpServletResponse response) {
                  System.out.println("hello cors");
                  return "hello cors";
              }

          }

          并配置啟動(dòng)端口為 80。

          編寫跨域請(qǐng)求前端

                
                <div id="test"></div>
          <input type="button" value="簡(jiǎn)單請(qǐng)求" onclick="simpleRequest()"/>
          </body>
          <script>

              function simpleRequest() {
                  $.ajax({
                      url:'http://edas.aliyun.com/testCors',
                      type:'get',
                      success:function (msg) {
                          $("#test").html(msg);
                      }
                  })
              }
          </script>

          配置 hosts

                
                127.0.0.1 edas.aliyun.com
          127.0.0.1 edasnext.aliyun.com

          為了方便在本地復(fù)現(xiàn)跨域問題,使用同一個(gè)后端,配置了兩個(gè)域名解析,edasnext.aliyun.com作為前端訪問的入口,edas.aliyun.com則作為后端接口的入口,由此構(gòu)建一個(gè)跨域場(chǎng)景。

          跨域測(cè)試

          830020cef1e5b448983f6cd6f6336ba6.webp bda033c5a254d1c8bd98d334b81d2343.webpimg

          可以看到,由于當(dāng)前的 springboot 應(yīng)用沒有進(jìn)行跨域配置,所以請(qǐng)求失敗了。

          而如果通過 postman 重放這次請(qǐng)求,請(qǐng)求成功:

          22a879befcce93937e67141fe7071160.webp

          這個(gè)實(shí)驗(yàn)得出了兩個(gè)結(jié)論:

          • 瀏覽器提示跨域請(qǐng)求失敗,服務(wù)端可能已經(jīng)處理完畢,但是由于沒有攜帶 Access 相關(guān)響應(yīng)頭,在到達(dá)瀏覽器時(shí),被拒絕了
          • 跨域問題的主體是瀏覽器,服務(wù)端是配合的角色

          服務(wù)端跨域配置

          如果僅僅是應(yīng)對(duì)簡(jiǎn)單請(qǐng)求模式,完全可以直接給響應(yīng)添加 Access-Control-Allow-Origin響應(yīng)頭,但實(shí)際的跨域全場(chǎng)景,流程比較復(fù)雜,springboot 提供了專門的跨域配置解決該問題:

                
                @Configuration
          public class CorsConfig implements WebMvcConfigurer {

              @Override
              public void addCorsMappings(CorsRegistry registry) {
                  registry.addMapping("/testCors")
                  .allowedOrigins("http://edasnext.aliyun.com")
                  .allowedMethods("*")
                  .allowedHeaders("*")
                  .allowCredentials(true)
                  .maxAge(3600);
              }
          }

          關(guān)于上述 Configuration 中的 CorsRegistry 跨域配置的最佳實(shí)踐,將會(huì)在下文中進(jìn)行詳細(xì)介紹。

          再次發(fā)起跨域請(qǐng)求測(cè)試成功

          a90c19db65969a93769653e0690342d0.webp

          至此簡(jiǎn)單請(qǐng)求模式介紹完畢。

          預(yù)檢請(qǐng)求模式

          690545b803bb6e87d2bb1e7483b97f55.webp

          預(yù)檢請(qǐng)求模式相比簡(jiǎn)單請(qǐng)求模式會(huì)多出一個(gè) OPTIONS 請(qǐng)求的流程,這個(gè)行為也是瀏覽器自主產(chǎn)生的。預(yù)檢請(qǐng)求的必要性主要在于更加安全,方便服務(wù)端針對(duì)復(fù)雜跨域請(qǐng)求進(jìn)行自主的校驗(yàn),并且減少了不必要的非正常跨域請(qǐng)求,缺點(diǎn)自然是加大了 CORS 的復(fù)雜度。

          預(yù)檢請(qǐng)求成功時(shí),瀏覽器會(huì)接受到預(yù)檢響應(yīng)中的信息,該信息包含了是否允許攜帶 cookie 以及預(yù)檢的緩存時(shí)間,這兩個(gè)參數(shù)都是極其有意義的,前者會(huì)在下文繼續(xù)補(bǔ)充,而后者決定了在一段時(shí)間內(nèi),針對(duì)復(fù)雜請(qǐng)求是否仍要發(fā)送預(yù)檢請(qǐng)求。

          預(yù)檢請(qǐng)求模式-快速入門

          編寫跨域前端

                
                <div id="test"></div>
          <input type="button" value="非簡(jiǎn)單請(qǐng)求" onclick="preflightedRequest()"/>
          </body>
          <script>

              function preflightedRequest() {
                  $.ajax({
                      url:'http://edas.aliyun.com/testCors',
                      type: 'post',
                      data: JSON.stringify({}),
                      contentType: 'application/json',
                      success:function (msg) {
                          $("#test").html(msg);
                      }
                  })
              }
          </script>

          測(cè)試非簡(jiǎn)單請(qǐng)求

          a0a45d2a42cbefee1091c6a28f878376.webp

          由于服務(wù)端已經(jīng)配置過跨域了,能夠配合瀏覽器正常處理預(yù)檢,可以看到瀏覽器先發(fā)送了一次預(yù)檢請(qǐng)求,后發(fā)送了實(shí)際請(qǐng)求。

          CORS 跨域配置的最佳實(shí)踐

          以 springboot 提供的 CorsRegistry 跨域配置為例,服務(wù)端在處理 CORS 跨域時(shí)一般有以下配置:

                
                corsRegistry.addMapping("/**")
                      .allowedOrigins("http://edasnext.aliyun.com")
                      .allowedMethods("*")
                      .allowedHeaders("*")
                      .allowCredentials(true)
                      .maxAge(3600);

          其中 allowedOrigins 和 allowCredentials 字段需要格外關(guān)注

          • allowedOrigins 表明允許哪些跨域來源允許訪問該服務(wù)端,與 HTTP 請(qǐng)求中的 Origin 請(qǐng)求頭對(duì)應(yīng)
          • allowCredentials 表明跨域請(qǐng)求是否可以攜帶 cookie

          一個(gè)跨域配置的誤區(qū)是配置 allowedOrigins=* 同時(shí)配置 allowCredentials=true

                
                // 錯(cuò)誤的示例
          corsRegistry.addMapping("/**")
                      .allowedOrigins("*")
                      .allowedMethods("*")
                      .allowedHeaders("*")
                      .allowCredentials(true)
                      .maxAge(3600);

          這表明允許任何來源可以進(jìn)行跨域請(qǐng)求,并且允許攜帶 cookie。

          瀏覽器的同源策略是基于安全考慮而設(shè)置的約束,避免了 CSRF、XSS 等常見的低成本的攻擊手段,所以并不能簡(jiǎn)單認(rèn)為跨域請(qǐng)求不被瀏覽器攔截就完事大吉了,需要做的是在沒有安全漏洞的前提下保證正常跨域請(qǐng)求能夠訪問成功。

          在 springboot 框架下,允許進(jìn)行上述的配置,但是實(shí)際處理請(qǐng)求時(shí),服務(wù)端會(huì)出現(xiàn)報(bào)錯(cuò):

          java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.] with root cause

          這也不見得是一個(gè)最佳實(shí)踐,更加建議在配置時(shí)就給出 ERROR 或者 WARN 配置提示,而不是延遲到運(yùn)行時(shí)報(bào)錯(cuò)。

          服務(wù)端跨域的行為并沒有明確地被 w3c 規(guī)范規(guī)定,這導(dǎo)致每個(gè)框架都有自己的配置格式以及表現(xiàn),也有框架默許這樣的錯(cuò)誤配置。或許這些框架在正常情況下,跨域請(qǐng)求不會(huì)被攔截,這種暴風(fēng)雨前的寧靜讓框架使用者感到舒心,但遭遇 CSRF、XSS 等攻擊時(shí),沒有人會(huì)感謝這樣的“方便”。

          一些跨域配置的最佳實(shí)踐如下:

          1. 允許所有來源進(jìn)行跨域請(qǐng)求時(shí),不允許攜帶 cookie:
                
                corsRegistry.addMapping("/**")
                      .allowedOrigins("*")
                      .allowedMethods("*")
                      .allowedHeaders("*")
                      .allowCredentials(false)
                      .maxAge(3600);
          1. 允許指定來源進(jìn)行跨域請(qǐng)求時(shí),攜帶 cookie:
                
                corsRegistry.addMapping("/**")
                      .allowedOrigins("http://edasnext.aliyun.com")
                      .allowedMethods("*")
                      .allowedHeaders("*")
                      .allowCredentials(true)
                      .maxAge(3600);
          1. 允許泛域名匹配時(shí)攜帶 cookie:
                
                corsRegistry.addMapping("/**")
          .allowedOriginPatterns("http://*.aliyun.com")
          .allowedMethods("*")
          .allowedHeaders("*")
          .allowCredentials(true)
          .maxAge(3600);

          當(dāng)子域名無法枚舉時(shí),可以使用這種匹配方式,但不要配置為 http://*.com或者 *

          1. 針對(duì)指定的服務(wù)端路徑進(jìn)行精細(xì)化的跨域配置
                
                corsRegistry.addMapping("/testCors")
                      .allowedOrigins("http://edasnext.aliyun.com")
                      .allowedMethods("*")
                      .allowedHeaders("*")
                      .allowCredentials(true)
                      .maxAge(3600);

          網(wǎng)關(guān)中的跨域配置

          在稍微復(fù)雜的系統(tǒng)架構(gòu)中,往往會(huì)引入網(wǎng)關(guān)組件,跨域配置也是非常適合轉(zhuǎn)移到網(wǎng)關(guān)的一項(xiàng)配置,可以將跨域的復(fù)雜度和安全性保障,收斂到網(wǎng)關(guān)這一個(gè)單一組件中。

          網(wǎng)關(guān)處理跨域問題,請(qǐng)求鏈路為:瀏覽器 -> 網(wǎng)關(guān) -> 服務(wù)端,根據(jù)前面的結(jié)論,同源策略只存在于瀏覽器發(fā)起的請(qǐng)求,所以網(wǎng)關(guān)向服務(wù)端的請(qǐng)求時(shí)不存在跨域問題,只需要考慮瀏覽器 -> 網(wǎng)關(guān)這一跳。

          以 Spring Cloud Gateway 為例,其提供了開箱即用的跨域能力:

                
                spring:
            cloud:
              gateway:
                fail-on-route-definition-error: false
                routes:
                - id: r-cors
                  predicates:
                    - Path=/testCors
                  uri: http://edas.aliyun.com
                  order: 1000
                globalcors:
                  cors-configurations:
                    '[/**]':
                      allowedOrigins: 'http://edasnext.aliyun.com'
                      allowCredentials: true
                      allowedMethods:
                        - '*'
                      allowedHeaders:
                        - '*'
                      maxAge: 3600
                  add-to-simple-url-handler-mapping: true

          在上述示例中,我們配置了一個(gè) r-cors 路由,轉(zhuǎn)發(fā)到本地的后端服務(wù),并且配置了全局的跨域策略,和服務(wù)端的配置格式類似。

          add-to-simple-url-handler-mapping的含義是:如果路由沒有配置 OPTIONS 匹配,可以打開此開關(guān),讓預(yù)檢請(qǐng)求成功返回,不會(huì)因?yàn)榭缬虻念A(yù)檢而導(dǎo)致路由訪問不通。推薦打開此配置,這樣在配置路由時(shí),可以只需要關(guān)注業(yè)務(wù)真正的請(qǐng)求方法,而不需要考慮跨域問題。

          在網(wǎng)關(guān)配置跨域后,后端服務(wù)就可以不用處理跨域問題了,去除服務(wù)端的 corsRegistry 配置,并且增加請(qǐng)求 Spring Cloud Gateway 網(wǎng)關(guān)的測(cè)試用例(本地將網(wǎng)關(guān)啟動(dòng)在 8080 端口):

                
                
                  <div id="test">
                  </div>
                  
          <input type="button" value="簡(jiǎn)單請(qǐng)求-網(wǎng)關(guān)" onclick="simpleRequestPassbyGateway()"/>
          <input type="button" value="非簡(jiǎn)單請(qǐng)求-網(wǎng)關(guān)" onclick="preflightedRequestPassbyGateway()"/>
          </body>
          <script>

            function simpleRequestPassbyGateway() {
              $.ajax({
                url:'http://127.0.0.1:8080/testCors',
                type:'get',
                data:{},
                success:function (msg{
                  $("#test").html(msg);
                }
              })
            }


            function preflightedRequestPassbyGateway() {
              $.ajax({
                url:'http://127.0.0.1:8080/testCors',
                type'post',
                dataJSON.stringify({ }),
                contentType'application/json',
                success:function (msg{
                  $("#test").html(msg);
                }
              })
            }
          </script>

          simpleRequestPassbyGateway,preflightedRequestPassbyGateway 會(huì)經(jīng)過網(wǎng)關(guān)路由轉(zhuǎn)發(fā)至后端服務(wù),根據(jù)同源策略 origin=127.0.0.1:8080,而前端域名為 http://edasnext.aliyun.com,這同樣是一個(gè)跨域請(qǐng)求。

          02ba86bfc9fda359a2e123e00c631a72.webp

          至此,只需要在網(wǎng)關(guān)進(jìn)行統(tǒng)一配置跨域,后端服務(wù)就不用關(guān)注跨域問題了。所以,跨域的支持也是主流網(wǎng)關(guān)的常用功能之一。

          網(wǎng)關(guān)跨域和服務(wù)端跨域共存的問題

          試想一下,如果服務(wù)端配置了跨域,同時(shí)網(wǎng)關(guān)配置跨域,表現(xiàn)會(huì)如何呢?這種場(chǎng)景一定不會(huì)少,例如一個(gè)原本配置了跨域的應(yīng)用,需要接入到網(wǎng)關(guān),一定會(huì)存在兩份跨域配置共存的時(shí)機(jī)。還是延續(xù)上述的跨域用例,打開服務(wù)端的 CorsRegistry 配置,再發(fā)送跨域請(qǐng)求至網(wǎng)關(guān),會(huì)得到如下報(bào)錯(cuò):

          Access to XMLHttpRequest at 'http://127.0.0.1:8080/testCors' from origin 'http://edasnext.aliyun.com' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'http://edasnext.aliyun.com, http://edasnext.aliyun.com', but only one is allowed.

          這是因?yàn)榫W(wǎng)關(guān)和服務(wù)端都會(huì)給響應(yīng)追加跨域請(qǐng)求頭,導(dǎo)致瀏覽器無法識(shí)別。

          312cf13434c03753c5b632fc9e118743.webp

          一個(gè)比較簡(jiǎn)單的開源解決方案是在網(wǎng)關(guān)上配置一個(gè)過濾器:

                
                spring:
            cloud:
              gateway:
             default-filters:
                  - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

          此方案可以去除重復(fù)的跨域響應(yīng)頭。

          在共存階段后完成流量遷移,最后建議還是去除服務(wù)端的配置。


          瀏覽 276
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐成人免费视频 | 国产精品日韩无码有码 | 久久视频这里有精品 | 操你逼逼 | 99精品视频免费观看, |