跨域訪問方法介紹(7)--使用 CORS


CORS 是一個 W3C 標准,全稱是"跨域資源共享"(Cross-origin resource sharing)。它允許瀏覽器向跨源服務器,發出 XMLHttpRequest 請求,從而克服了 AJAX 只能同源使用的限制。本文主要介紹 CORS 的基本使用,文中所使用到的軟件版本:Chrome 90.0.4430.212、jquery 1.12.4,Spring Boot 2.4.4、jdk1.8.0_181。

1、CORS 簡介

CORS 需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於 IE10。整個 CORS 通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣。瀏覽器一旦發現 AJAX 請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。因此,實現 CORS 通信的關鍵是服務器。只要服務器實現了 CORS 接口,就可以跨源通信。

1.1、兩種請求

瀏覽器將 CORS 請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。只要同時滿足以下兩大條件,就屬於簡單請求。

(1)請求方法是以下三種方法之一:
 HEAD
 GET
 POST
(2)HTTP的頭信息不超出以下幾種字段:
 Accept
 Accept-Language
 Content-Language
 Last-Event-ID
 Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain

這是為了兼容表單(form),因為歷史上表單一直可以發出跨域請求。AJAX 的跨域設計就是,只要表單可以發,AJAX 就可以直接發。凡是不同時滿足上面兩個條件,就屬於非簡單請求。瀏覽器對這兩種請求的處理,是不一樣的。

1.2、簡單請求

1.2.1、基本流程

對於簡單請求,瀏覽器直接發出 CORS 請求。瀏覽器發現跨源 AJAX 請求是簡單請求,就自動在頭信息之中,添加一個 Origin 字段。

Host: localhost:8081
Origin: http://localhost:8080
Pragma: no-cache
User-Agent: Mozilla/5.0  ...
Connection: keep-alive

上面的頭信息中,Origin 字段用來說明本次請求來自哪個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否同意這次請求。如果 Origin 指定的源,不在許可范圍內,服務器會返回一個正常的HTTP 響應。瀏覽器發現,這個響應的頭信息沒有包含 Access-Control-Allow-Origin 字段,就知道出錯了,從而拋出一個錯誤,被 XMLHttpRequest 的 onerror 回調函數捕獲。注意,這種錯誤無法通過狀態碼識別,因為 HTTP 響應的狀態碼有可能是 200。如果 Origin 指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段。

Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: http://localhost:8080 Access-Control-Expose-Headers: my-response-header
Connection: keep-alive
Content-Length: 12
Content-Type: text/plain;charset=UTF-8

上面的頭信息之中,有三個與 CORS 請求相關的字段,都以 Access-Control- 開頭。

(1) Access-Control-Allow-Origin
該字段是必須的。它的值要么是請求時 Origin 字段的值,要么是一個 *,表示接受任意域名的請求。
(2) Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否允許發送 Cookie。默認情況下,Cookie 不包括在 CORS 請求之中。設為 true,即表示服務器明確許可,Cookie 可以包含在請求中一起發給服務器。這個值也只能設為 true,如果服務器不要瀏覽器發送 Cookie,刪除該字段即可。
(3) Access-Control-Expose-Headers
該字段可選。CORS 請求時,XMLHttpRequest 對象的 getResponseHeader() 方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必須在 Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader('myResponseHeader') 可以返回 myResponseHeader 字段的值。

1.2.2、withCredentials 屬性

CORS 請求默認不發送 Cookie 和HTTP認證信息。如果要把 Cookie 發到服務器,一方面要服務器同意,指定 Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,必須在 AJAX 請求中打開 withCredentials 屬性。

let xhr = new XMLHttpRequest();
xhr.withCredentials = true

否則,即使服務器同意發送 Cookie,瀏覽器也不會發送。或者,服務器要求設置 Cookie,瀏覽器也不會處理。如果省略 withCredentials 設置,有的瀏覽器還是會一起發送 Cookie;可以顯式關閉withCredentials。

xhr.withCredentials = false;

 需要注意的是,如果要發送 Cookie,Access-Control-Allow-Origin 就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie 依然遵循同源政策,只有用服務器域名設置的 Cookie 才會上傳,其他域名的 Cookie 並不會上傳,且(跨源)原網頁代碼中的 document.cookie 也無法讀取服務器域名下的 Cookie。

1.3、非簡單請求

1.3.1、預檢請求

非簡單請求是那種對服務器有特殊要求的請求,比如請求方法是 PUT 或 DELETE,或者 Content-Type 字段的類型是 application/json。非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為"預檢"請求(preflight)。瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些 HTTP 動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的 XMLHttpRequest 請求,否則就報錯。

下面是預檢請求的頭信息:

Access-Control-Request-Headers: my-request-header Access-Control-Request-Method: POST
Cache-Control: no-cache
Connection: keep-alive
Host: localhost:8081 Origin: http://localhost:8080

除了 Origin 字段,預檢請求的頭信息包括兩個特殊字段。
(1)Access-Control-Request-Method
該字段是必須的,用來列出瀏覽器的 CORS 請求會用到哪些HTTP方法,上例是 POST。
(2)Access-Control-Request-Headers
該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段,上例是 my-request-header

1.3.2、預檢請求的響應

服務器收到預檢請求以后,檢查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,確認允許跨源請求,就可以做出回應。

Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: my-request-header Access-Control-Allow-Methods: GET,POST Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Expose-Headers: my-response-header Access-Control-Max-Age: 1800
Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
Connection: keep-alive
Content-Length: 0
Date: Fri, 04 Jun 2021 02:55:51 GMT
Keep-Alive: timeout=60

上面的 HTTP 回應中,關鍵的是 Access-Control-Allow-Origin 字段,表示 http://localhost:8080 可以請求數據。該字段也可以設為星號,表示同意任意跨源請求。

如果服務器否定了預檢請求,會返回一個正常的 HTTP 回應,但是沒有任何 CORS 相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被 XMLHttpRequest 對象的 onerror 回調函數捕獲。

預檢請求響應其他 CORS 相關字段:

(1)Access-Control-Allow-Methods
該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。
(2)Access-Control-Allow-Headers
如果瀏覽器請求包括 Access-Control-Request-Headers 字段,則 Access-Control-Allow-Headers 字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在預檢中請求的字段。
(3)Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是 1800 秒,即允許緩存該條響應 1800 秒,在此期間,不用發出另一條預檢請求。
(4)Access-Control-Allow-Credentials
該字段與簡單請求時的含義相同。
(5)Access-Control-Expose-Headers
該字段與簡單請求時的含義相同。

1.3.3、預檢請求后簡單請求

一旦服務器通過了預檢請求,以后每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣。下面是預檢請求之后,正常的 CORS 請求:

Cache-Control: no-cache
Connection: keep-alive
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
Host: localhost:8081
my-request-header: 123
Origin: http://localhost:8080

服務器正常請求的響應:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Expose-Headers: my-response-header
Connection: keep-alive
Content-Length: 12
Content-Type: text/plain;charset=UTF-8

2、樣例

2.1、后台實現(SpringBoot 版)

先編寫后台 Controller:

package com.abc.demo.controller;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RequestMapping("/corstest")
@RestController
public class CorsTestController {
    @RequestMapping("/hello")
    public String hello(String name, HttpServletRequest request, HttpServletResponse response) {
        System.out.println("my-request-header=" + request.getHeader("my-request-header"));
        response.addHeader("my-response-header","abc");
        return "hello," + name;
    }
}

使用 SpringBoot 來實現 CORS,有三種實現方式,選其中一種即可。

2.1.1、使用 @CrossOrigin 注解

在 Controller 中使用 @CrossOrigin 注解。

package com.abc.demo.controller;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RequestMapping("/corstest")
@RestController
public class CorsTestController {
    @CrossOrigin(origins = "http://localhost:8080",
            allowCredentials = "true",
            methods = {RequestMethod.GET, RequestMethod.POST},
            allowedHeaders = {"my-request-header", "my-request-header2", "content-type"},
            exposedHeaders = {"my-response-header"})
    @RequestMapping("/hello")
    public String hello(String name, HttpServletRequest request, HttpServletResponse response) {
        System.out.println("my-request-header=" + request.getHeader("my-request-header"));
        response.addHeader("my-response-header","abc");
        return "hello," + name;
    }
}

2.1.2、實現 WebMvcConfigurer 接口

package com.abc.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("GET", "POST")
                .allowedOrigins("http://localhost:8080")
                .allowedHeaders("my-request-header", "my-request-header2", "content-type")
                .exposedHeaders("my-response-header")
                .allowCredentials(true);
    }
}

2.1.3、實現 Filter 接口

package com.abc.demo.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@WebFilter(filterName = "coreFilter", urlPatterns = "/*")
public class CoreFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
        response.addHeader("Access-Control-Allow-Credentials", "true");
        response.addHeader("Access-Control-Expose-Headers", "my-response-header");

        if (((HttpServletRequest) servletRequest).getMethod().equals("OPTIONS")) {
            response.addHeader("Access-Control-Allow-Methods", "GET,POST");
            response.addHeader("Access-Control-Allow-Headers", "my-request-header,my-request-header2,content-type");
            response.addHeader("Access-Control-Max-Age", "1800");
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

2.2、前台實現

2.2.1、原生寫法(corstest.html)

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>cors 測試</title>

</head>
<body>
</body>

<script type="text/javascript">
    // IE8/9需用window.XDomainRequest兼容
    let xhr = new XMLHttpRequest(); 
     
    xhr.withCredentials = true;
    
    xhr.open('post', 'http://localhost:8081/corstest/hello', true);
    xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    //xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.setRequestHeader('my-request-header', '123');
    xhr.send('name=李白');
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && xhr.status == 200) {
            alert(xhr.responseText + "|" + xhr.getResponseHeader('my-response-header'));
        }
    }
</script>
</html>

2.2.2、Jquery寫法(corstestJquery.html)

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>cors 測試</title>

</head>
<body>
</body>
<script type="text/javascript" src="./jquery-1.12.4.min.js"></script>
<script type="text/javascript">
    $(function(){
        $.ajax({
            url: 'http://localhost:8081/corstest/hello',
            type: 'post',
            contentType: 'application/x-www-form-urlencoded',
            headers: {
                'my-request-header': '123'
            },
            data: {
                name: '李白'
            },
            xhrFields: {
                withCredentials: true
            },
            crossDomain: true,
            success: function(data, status, xhr) {
                alert(data + '|' + xhr.getResponseHeader('my-response-header'));
            }
        });
    });

</script>
</html>

3、測試

把 corstest.html 和 corstestJquery.html 放到 tomcat(端口:8080) 的 webapps\ROOT 下,並啟動 SpringBoot 應用(端口:8081)。

 

 

參考:http://www.ruanyifeng.com/blog/2016/04/cors.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM