Spring Boot - 跨域資源共享(CORS)

同源策略

在瀏覽器中,如果我們直接使用 AJAX 發(fā)送一個(gè)對(duì)其他網(wǎng)站的請(qǐng)求(跨域請(qǐng)求),默認(rèn)情況下是無(wú)法獲取到響應(yīng)的。
這是因?yàn)闉g覽器內(nèi)置的 同源策略 對(duì)客戶端腳本的限制。

默認(rèn)情況下,同源策略 只允許腳本請(qǐng)求同源資源,而對(duì)于請(qǐng)求不同源的腳本在沒(méi)有明確授權(quán)的情況下,無(wú)法讀取對(duì)方資源。

同源 指的是:協(xié)議、域名端口 三者都相同

同源策略是瀏覽器內(nèi)置的一個(gè)最核心,也是最基礎(chǔ)的安全功能,它保障了用戶的上網(wǎng)安全。

但是,如果我們確信某個(gè)非同源網(wǎng)站是安全的,我們希望能夠?qū)ζ滟Y源進(jìn)行訪問(wèn),那么,就需要通過(guò)相應(yīng)的機(jī)制進(jìn)行跨域請(qǐng)求。

最常見(jiàn)的前端跨域請(qǐng)求解決方案是 JSONP,它的原理是借助script標(biāo)簽不受瀏覽器同源策略限制,允許跨域請(qǐng)求資源,因此可以通過(guò)script標(biāo)簽的src屬性,進(jìn)行跨域訪問(wèn)。如下代碼所示:

// 1. 前端定義一個(gè) 回調(diào)函數(shù) handleResponse 用來(lái)接收后端返回的數(shù)據(jù)
function handleResponse(data) {
    console.log(data);
};

// 2. 動(dòng)態(tài)創(chuàng)建一個(gè) script 標(biāo)簽,并且告訴后端回調(diào)函數(shù)名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://www.laixiangran.cn/json?callback=handleResponse';
body.appendChild(script);

// 3. 通過(guò) script.src 請(qǐng)求 `http://www.laixiangran.cn/json?callback=handleResponse`,
// 4. 后端能夠識(shí)別這樣的 URL 格式并處理該請(qǐng)求,然后返回 handleResponse({"name": "laixiangran"}) 給瀏覽器
// 5. 瀏覽器在接收到 handleResponse({"name": "laixiangran"}) 之后立即執(zhí)行 ,也就是執(zhí)行 handleResponse 方法,獲得后端返回的數(shù)據(jù),這樣就完成一次跨域請(qǐng)求了。

雖然 JSONP 可以完成跨域請(qǐng)求,但是它只支持GET請(qǐng)求方式,限制非常大。
于是,為了更好地支持跨域資源請(qǐng)求,W3C 標(biāo)準(zhǔn)就發(fā)布了一套瀏覽器跨域資源共享標(biāo)準(zhǔn):CORS(Cross-origin resource sharing,跨域資源共享)

CORS(跨域資源共享)

CORS 支持多種 HTTP 請(qǐng)求,它其實(shí)就是定義了一套跨域資源請(qǐng)求時(shí),瀏覽器與服務(wù)器之間的交互方式。基本的原理就是通過(guò)自定義的 HTTP 請(qǐng)求頭來(lái)傳遞信息,進(jìn)行驗(yàn)證。

瀏覽器中,將 CORS 請(qǐng)求分為兩種類(lèi)型:

  • 簡(jiǎn)單請(qǐng)求:同時(shí)滿足以下兩大條件的請(qǐng)求,即為簡(jiǎn)單請(qǐng)求:

    1. 請(qǐng)求的方法是HEADGET或者是POST三種之一
    2. 請(qǐng)求頭不超出以下幾種字段:Accept、Accept-Language、Content-LanguageLast-Event-ID、Content-Type(其值為:application/x-www-form-urlencoded、multipart/form-datatext/plain三者之一)
  • 非簡(jiǎn)單請(qǐng)求:不是簡(jiǎn)單請(qǐng)求的都屬于非簡(jiǎn)單請(qǐng)求。

瀏覽器對(duì)于 簡(jiǎn)單請(qǐng)求 和 非簡(jiǎn)單請(qǐng)求 的 CORS 處理機(jī)制不一樣,具體如下:

  • 簡(jiǎn)單請(qǐng)求:對(duì)于簡(jiǎn)單請(qǐng)求的 CORS,瀏覽器的處理機(jī)制流程如下:

    1. 瀏覽器會(huì)在請(qǐng)求頭添加一個(gè)額外的Origin頭部,其值為當(dāng)前請(qǐng)求頁(yè)面的源信息(即:協(xié)議 + 域名 + 端口)。如下所示:
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    
    1. 服務(wù)器接收到請(qǐng)求后,查看到Origin頭部指定的源信息,如果同意該請(qǐng)求,就會(huì)為下發(fā)的響應(yīng)添加頭部Access-Control-Allow-Origin,其值為請(qǐng)求的源信息(或者是*,表示允許任意源信息)。如下所示:
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8
    
    1. 瀏覽器接收到響應(yīng)后,會(huì)查看下是否有Access-Control-Allow-Origin頭部信息,如果沒(méi)有或者其值不匹配當(dāng)前源信息,那么瀏覽器就會(huì)禁止響應(yīng)該 CORS 請(qǐng)求,當(dāng)前頁(yè)面的 AJAX 請(qǐng)求的onerror函數(shù)會(huì)得到回調(diào)。
      反之,如果瀏覽器驗(yàn)證通過(guò),則跨域請(qǐng)求成功。

    :CORS 請(qǐng)求默認(rèn)不發(fā)送 Cookie 和 HTTP 認(rèn)證信息,如果需要把 Cookie 發(fā)送給服務(wù)器,則 AJAX 和 服務(wù)器必須同時(shí)打開(kāi) Credentials 字段,如下所示:

    • 服務(wù)器需設(shè)置:Access-Control-Allow-Credentials: true
    • AJAX 需設(shè)置:new XMLHttpRequest().withCredentials = true;

    :如果 AJAX 發(fā)送了 Cookie,那么服務(wù)器的Access-Control-Allow-Origin則不能設(shè)置為*,必須指定該明確的、與請(qǐng)求網(wǎng)頁(yè)一致的域名。

  • 非簡(jiǎn)單請(qǐng)求:非簡(jiǎn)單請(qǐng)求是那種對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如PUTDELETE請(qǐng)求,或者是Content-type: application/json請(qǐng)求...
    瀏覽器檢測(cè)到非簡(jiǎn)單請(qǐng)求的 CORS 時(shí),在正式發(fā)送請(qǐng)求前,會(huì)先進(jìn)行一次探測(cè)請(qǐng)求(preflight),通過(guò)才會(huì)發(fā)送正式請(qǐng)求,具體過(guò)程如下:

    1. 瀏覽器檢測(cè)到非簡(jiǎn)單 CORS 請(qǐng)求,則先發(fā)送一個(gè)探測(cè)請(qǐng)求,請(qǐng)求方式為OPTIONS,如下所示:
    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    可以看到OPTIONS請(qǐng)求,除了攜帶Origin請(qǐng)求頭外,還額外攜帶了以下幾個(gè)請(qǐng)求頭:

    • Access-Control-Request-Method:該字段必須攜帶,表示 CORS 請(qǐng)求使用的 HTTP 請(qǐng)求方法
    • Access-Control-Request-Headers:可選字段,表示 CORS 請(qǐng)求發(fā)送的自定義頭部信息,多個(gè)頭部以逗號(hào)進(jìn)行分隔
    1. 服務(wù)器收到瀏覽器發(fā)送的探測(cè)請(qǐng)求后,檢測(cè)Origin、Access-Control-Request-MethodAccess-Control-Request-Headers都在自己的許可名單時(shí),就會(huì)允許跨域請(qǐng)求,返回響應(yīng)。如下所示:
    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Max-Age: 1728000
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain
    

    響應(yīng)主要包含如下請(qǐng)求頭信息:

    • Access-Control-Allow-Origin:表示允許進(jìn)行跨域請(qǐng)求的域
    • Access-Control-Allow-Methods:必須字段,表示允許 CORS 請(qǐng)求的方法
    • Access-Control-Allow-Headers:表示允許 CORS 請(qǐng)求的頭部
    • Access-Control-Max-Age:表示探測(cè)請(qǐng)求緩存時(shí)間(單位:秒)
    1. 一旦瀏覽器通過(guò)探測(cè)請(qǐng)求,以后每次進(jìn)行 CORS 請(qǐng)求時(shí),就重復(fù)簡(jiǎn)單請(qǐng)求步驟(直至探測(cè)請(qǐng)求緩存過(guò)期)。
      而如果探測(cè)請(qǐng)求通不過(guò)(即響應(yīng)沒(méi)有任何 CORS 相關(guān)的頭部信息字段),瀏覽器就知道服務(wù)器會(huì)拒絕該 CORS 請(qǐng)求,于是就直接觸發(fā)一個(gè)錯(cuò)誤,回調(diào)給 AJAX 請(qǐng)求的onerror方法。

Spring Boot 配置支持 CORS

一個(gè)很幸運(yùn)的事情就是:瀏覽器會(huì)自動(dòng)幫我們完成 CORS 相關(guān)操作,用戶完全無(wú)感知。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō),前端代碼無(wú)需修改,如果是 CORS 請(qǐng)求,瀏覽器會(huì)自動(dòng)幫我們加上相應(yīng)的請(qǐng)求頭進(jìn)行請(qǐng)求...

因此,實(shí)現(xiàn) CORS 通信需要配置的就只是服務(wù)器端。

下面介紹下在 Spring Boot 中配置 CORS 通信,主要介紹幾種常用的配置方法,如下所示:

  • @CrossOrigin:該注解可用于方法和類(lèi)上,注解在方法上,表示對(duì)該方法的請(qǐng)求進(jìn)行 CORS 校驗(yàn),注解在類(lèi)上(即Controller上),表示該類(lèi)內(nèi)的方法都遵循該 CORS 校驗(yàn)。如下所示:

    :前端頁(yè)面 AJAX 請(qǐng)求源碼可查看 附錄 內(nèi)容。

    @Slf4j
    @RestController
    @RequestMapping("cors")
    @CrossOrigin(
            value = "http://127.0.0.1:5500",
            maxAge = 1800,
            allowedHeaders = "*")
    public class CorsController {
    
        @PostMapping("/")
        public String add(@RequestParam("name") String name,
                          @RequestHeader("Origin") String origin) {
            log.info("Request Header ==> Origin: " + origin);
            return "add successfully: " + name;
    
        }
    
        @DeleteMapping("/{id}")
        public String delete(@PathVariable("id") Long id) {
            return String.valueOf(id) + " deleted!";
        }
    }
    

    上述代碼在Controller類(lèi)上使用@CrossOrigin進(jìn)行注解配置 CORS,這樣前端頁(yè)面就可以進(jìn)行 CORS 請(qǐng)求當(dāng)前Controller下的所有接口。

    其中,@CrossOrigin注解可選參數(shù)如下:

    方法 作用
    value 表示支持的域,即Access-Control-Allow-Origin的值
    origins 表示支持的域數(shù)組
    methods 表示支持的 CORS 請(qǐng)求方法,即Access-Control-Allow-Methods的值。
    其默認(rèn)值與綁定的控制器方法一致
    maxAge 表示探測(cè)請(qǐng)求緩存時(shí)間(單位:秒),即Access-Control-Max-Age的值。
    其默認(rèn)值為1800,也即 30 分鐘
    allowedHeaders 表示允許的請(qǐng)求頭,即Access-Control-Allow-Headers的值
    默認(rèn)情況下,支持所有請(qǐng)求頭
    exposedHeaders 表示下發(fā)其他響應(yīng)頭字段給瀏覽器,即Access-Control-Expose-Headers的值。
    默認(rèn)不下發(fā)暴露字段
    allowCredentials 表示是否支持瀏覽器發(fā)送認(rèn)證信息(比如 Cookie),即Access-Control-Allow-Credentials的值。
    默認(rèn)不支持接收認(rèn)證信息
  • 全局配置:如果想全局配置 CORS 通信,只需添加一個(gè)配置類(lèi)。如下所示:

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")     //設(shè)置允許跨域的路徑
                    .allowedOrigins("*")
                    .allowedMethods("*")
                    .allowedHeaders("*")
                    .maxAge(1800)
                    .allowCredentials(true);
        }
    }
    

    只需創(chuàng)建一個(gè)配置類(lèi)實(shí)現(xiàn)接口WebMvcConfigurer,然后覆寫(xiě)方法addCorsMappings即可。
    addCorsMappings方法中,registry.addMapping用于設(shè)置可以進(jìn)行跨域請(qǐng)求的路徑,比如/cors/**表示路徑/cors/下的所有路由都支持 CORS 請(qǐng)求。其他的設(shè)置與注解@CrossOrigin一樣,無(wú)需介紹。

    :這里也可以直接通過(guò)注入一個(gè)WebMvcConfigurer的 Bean 實(shí)例,自定義跨域規(guī)則:

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS");
            }
        };
    }
    
  • 通過(guò)Filter配置:通過(guò)過(guò)濾器Filter可以讓我們手動(dòng)控制響應(yīng),自然就能完成 CORS 配置。如下所示:

    @Component
    public class CorsFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
    
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    

附錄

  • CORS 前端頁(yè)面 AJAX 請(qǐng)求源碼如下所示:

    <html lang="en">
      <!-- ... -->
      <body>
        <button id="cors_post">CORS - POST</button>
        <button id="cors_delete">CORS - DELETE</button>
    
        <script>
          const BASE_URL = 'http://localhost:8080/cors/';
          const postBtn = document.querySelector('#cors_post');
          postBtn.addEventListener('click', async () => {
            // 簡(jiǎn)單請(qǐng)求
            const response = await fetch(BASE_URL, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
              },
              body: 'name=Whyn',
            });
            response.text().then((text) => console.log(text));
          });
    
          const delBtn = document.querySelector('#cors_delete');
          delBtn.addEventListener('click', async () => {
            // 非簡(jiǎn)單請(qǐng)求
            const response = await fetch(BASE_URL + '1', {
              method: 'DELETE',
            });
            response.text().then((text) => console.log(text));
          });
        </script>
      </body>
    </html>
    

    :前端頁(yè)面運(yùn)行在本地:http://127.0.0.1:5500

  • Spring Security 配置跨域:如果項(xiàng)目中使用了 Spring Security 框架,那么也可以直接配置 Spring Security 支持跨域即可:

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 允許跨域資源請(qǐng)求
            // by default uses a Bean by the name of corsConfigurationSource
            http.cors(Customizer.withDefaults());
        }
    
        @Bean
        CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("*"));
            configuration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS"));
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            // 所有 url 都使用 configuration 定制的跨域規(guī)則
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    }
    

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容