為 RESTful Web 服務啟用跨來源請求

本指南將引導您完成使用 Spring 建立 “Hello, World” RESTful Web 服務的流程,該服務在回應中包含跨來源資源共享 (CORS) 的標頭。您可以在這篇部落格文章中找到更多關於 Spring CORS 支援的資訊。

您將建置的內容

您將建置一個服務,該服務接受在 https://127.0.0.1:8080/greeting 的 HTTP GET 請求,並以 JSON 格式回應問候語,如下列清單所示

{"id":1,"content":"Hello, World!"}

您可以使用查詢字串中的選用 name 參數來自訂問候語,如下列清單所示

https://127.0.0.1:8080/greeting?name=User

name 參數值會覆寫預設值 World,並反映在回應中,如下列清單所示

{"id":1,"content":"Hello, User!"}

此服務與建置 RESTful Web 服務中描述的服務略有不同,因為它使用 Spring Framework CORS 支援來新增相關的 CORS 回應標頭。

您需要的東西

如何完成本指南

如同大多數 Spring 入門指南,您可以從頭開始並完成每個步驟,或者您可以跳過您已熟悉的基礎設定步驟。無論哪種方式,您最終都會得到可運作的程式碼。

若要從頭開始,請移至從 Spring Initializr 開始

若要跳過基礎知識,請執行下列操作

當您完成時,您可以根據 gs-rest-service-cors/complete 中的程式碼檢查您的結果。

從 Spring Initializr 開始

您可以使用這個預先初始化的專案,然後按一下 Generate 來下載 ZIP 檔案。此專案已設定為符合本教學課程中的範例。

若要手動初始化專案

  1. 導覽至 https://start.spring.io。此服務會提取應用程式所需的所有相依性,並為您完成大部分的設定。

  2. 選擇 Gradle 或 Maven 以及您想要使用的語言。本指南假設您選擇 Java。

  3. 按一下Dependencies,然後選取 Spring Web

  4. 按一下Generate

  5. 下載產生的 ZIP 檔案,這是一個已使用您的選擇設定的 Web 應用程式的封存檔。

如果您的 IDE 具有 Spring Initializr 整合,您可以從您的 IDE 完成此流程。
您也可以從 Github 分支專案,並在您的 IDE 或其他編輯器中開啟它。

新增 httpclient5 相依性

測試 (在 complete/src/test/java/com/example/restservicecors/GreetingIntegrationTests.java 中) 需要 Apache httpclient5 程式庫。

若要將 Apache httpclient5 程式庫新增至 Maven,請新增下列相依性

<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
  <scope>test</scope>
</dependency>

下列清單顯示完成的 pom.xml 檔案

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>rest-service-cors-complete</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>rest-service-cors-complete</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.httpcomponents.client5</groupId>
			<artifactId>httpclient5</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

若要將 Apache httpclient5 程式庫新增至 Gradle,請新增下列相依性

testImplementation 'org.apache.httpcomponents.client5:httpclient5'

下列清單顯示完成的 build.gradle 檔案

plugins {
	id 'org.springframework.boot' version '3.3.0'
	id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.apache.httpcomponents.client5:httpclient5'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

建立資源表示類別

現在您已設定專案和建置系統,您可以建立您的 Web 服務。

從思考服務互動開始此流程。

服務將處理對 /greetingGET 請求,選擇性地在查詢字串中使用 name 參數。GET 請求應傳回 200 OK 回應,並在主體中使用 JSON 來表示問候語。它應類似於下列清單

{
    "id": 1,
    "content": "Hello, World!"
}

id 欄位是問候語的唯一識別碼,而 content 是問候語的文字表示。

為了模擬問候語表示,請建立一個資源表示類別。提供一個包含欄位、建構子和存取子的純舊 Java 物件,用於 idcontent 資料,如下列清單 (來自 src/main/java/com/example/restservicecors/Greeting.java) 所示

package com.example.restservicecors;

public class Greeting {

	private final long id;
	private final String content;

	public Greeting() {
		this.id = -1;
		this.content = "";
	}

	public Greeting(long id, String content) {
		this.id = id;
		this.content = content;
	}

	public long getId() {
		return id;
	}

	public String getContent() {
		return content;
	}
}
Spring 使用 Jackson JSON 程式庫來自動將 Greeting 類型的實例封送處理為 JSON。

建立資源控制器

在 Spring 建置 RESTful Web 服務的方法中,HTTP 請求由控制器處理。這些元件很容易透過 @Controller 註解來識別,而下列清單 (來自 src/main/java/com/example/restservicecors/GreetingController.java) 中顯示的 GreetingController 會處理對 /greetingGET 請求,方法是傳回 Greeting 類別的新實例

package com.example.restservicecors;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

	private static final String template = "Hello, %s!";

	private final AtomicLong counter = new AtomicLong();
	@CrossOrigin(origins = "https://127.0.0.1:9000")
	@GetMapping("/greeting")
	public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
		System.out.println("==== get greeting ====");
		return new Greeting(counter.incrementAndGet(), String.format(template, name));
	}

}

此控制器簡潔而簡單,但在底層有很多事情正在進行。我們逐步分解它。

@RequestMapping 註解確保對 /greeting 的 HTTP 請求會對應到 greeting() 方法。

先前的範例使用 @GetMapping 註解,它充當 @RequestMapping(method = RequestMethod.GET) 的捷徑。我們在此案例中使用 GET,因為它方便測試。如果來源與 CORS 設定不符,Spring 仍然會拒絕 GET 請求。瀏覽器不需要傳送 CORS 預檢請求,但如果我們想要觸發預檢檢查,我們可以改用 @PostMapping 並在主體中接受一些 JSON。

@RequestParamname 查詢字串參數的值繫結到 greeting() 方法的 name 參數。此查詢字串參數不是 required。如果請求中不存在,則會使用 WorlddefaultValue

方法主體的實作會建立並傳回新的 Greeting 物件,其中 id 屬性的值基於來自 counter 的下一個值,而 content 的值基於查詢參數或預設值。它還使用問候語 template 來格式化給定的 name

傳統 MVC 控制器與先前顯示的 RESTful Web 服務控制器之間的主要差異在於建立 HTTP 回應主體的方式。此 RESTful Web 服務控制器不是依賴檢視技術來執行問候語資料到 HTML 的伺服器端呈現,而是填入並傳回 Greeting 物件。物件資料會直接寫入 HTTP 回應作為 JSON。

為了實現這一點,@RestController 註解假設每個方法預設都會繼承 @ResponseBody 語意。因此,傳回的物件資料會直接插入回應主體中。

由於 Spring 的 HTTP 訊息轉換器支援,Greeting 物件自然會轉換為 JSON。由於 Jackson 在類別路徑上,Spring 的 MappingJackson2HttpMessageConverter 會自動選擇將 Greeting 實例轉換為 JSON。

啟用 CORS

您可以從個別控制器或全域啟用跨來源資源共享 (CORS)。以下主題描述如何執行此操作

控制器方法 CORS 設定

為了讓 RESTful Web 服務在其回應中包含 CORS 存取控制標頭,您必須將 @CrossOrigin 註解新增至處理常式方法,如下列清單 (來自 src/main/java/com/example/restservicecors/GreetingController.java) 所示

	@CrossOrigin(origins = "https://127.0.0.1:9000")
	@GetMapping("/greeting")
	public Greeting greeting(@RequestParam(required = false, defaultValue = "World") String name) {
		System.out.println("==== get greeting ====");
		return new Greeting(counter.incrementAndGet(), String.format(template, name));

@CrossOrigin 註解僅針對此特定方法啟用跨來源資源共享。依預設,它允許所有來源、所有標頭以及 @RequestMapping 註解中指定的 HTTP 方法。此外,還使用了 30 分鐘的 maxAge。您可以透過指定下列其中一個註解屬性的值來自訂此行為

  • origins

  • originPatterns

  • methods

  • allowedHeaders

  • exposedHeaders

  • allowCredentials

  • maxAge.

在本範例中,我們僅允許 https://127.0.0.1:9000 發送跨來源請求。

您也可以在控制器類別層級新增 @CrossOrigin 註解,以在此類別的所有處理常式方法上啟用 CORS。

全域 CORS 設定

除了 (或替代) 細緻的基於註解的設定之外,您還可以定義一些全域 CORS 設定。這類似於使用 Filter,但可以在 Spring MVC 中宣告,並與細緻的 @CrossOrigin 設定結合使用。依預設,允許所有來源和 GETHEADPOST 方法。

下列清單 (來自 src/main/java/com/example/restservicecors/GreetingController.java) 顯示 GreetingController 類別中的 greetingWithJavaconfig 方法

	@GetMapping("/greeting-javaconfig")
	public Greeting greetingWithJavaconfig(@RequestParam(required = false, defaultValue = "World") String name) {
		System.out.println("==== in greeting ====");
		return new Greeting(counter.incrementAndGet(), String.format(template, name));
greetingWithJavaconfig 方法與 greeting 方法 (在控制器層級 CORS 設定中使用) 之間的差異在於路由 (/greeting-javaconfig 而不是 /greeting) 和 @CrossOrigin 來源的存在。

下列清單 (來自 src/main/java/com/example/restservicecors/RestServiceCorsApplication.java) 顯示如何在應用程式類別中新增 CORS 對應

	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping("/greeting-javaconfig").allowedOrigins("https://127.0.0.1:9000");
			}
		};
	}

您可以輕鬆變更任何屬性 (例如範例中的 allowedOrigins),並將此 CORS 設定套用至特定的路徑模式。

您可以結合全域和控制器層級 CORS 設定。

建立應用程式類別

Spring Initializr 為您建立了一個最基本的應用程式類別。下列清單 (來自 initial/src/main/java/com/example/restservicecors/RestServiceCorsApplication.java) 顯示該初始類別

package com.example.restservicecors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RestServiceCorsApplication {

	public static void main(String[] args) {
		SpringApplication.run(RestServiceCorsApplication.class, args);
	}

}

您需要新增一個方法來設定如何處理跨來源資源共享。下列清單 (來自 complete/src/main/java/com/example/restservicecors/RestServiceCorsApplication.java) 顯示如何執行此操作

	@Bean
	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping("/greeting-javaconfig").allowedOrigins("https://127.0.0.1:9000");
			}
		};
	}

下列清單顯示完成的應用程式類別

package com.example.restservicecors;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class RestServiceCorsApplication {

	public static void main(String[] args) {
		SpringApplication.run(RestServiceCorsApplication.class, args);
	}

	@Bean
	public WebMvcConfigurer corsConfigurer() {
		return new WebMvcConfigurer() {
			@Override
			public void addCorsMappings(CorsRegistry registry) {
				registry.addMapping("/greeting-javaconfig").allowedOrigins("https://127.0.0.1:9000");
			}
		};
	}

}

@SpringBootApplication 是一個便利的註解,它新增了以下所有內容

  • @Configuration:將類別標記為應用程式內容的 Bean 定義來源。

  • @EnableAutoConfiguration:告知 Spring Boot 根據類別路徑設定、其他 Bean 和各種屬性設定開始新增 Bean。例如,如果 spring-webmvc 在類別路徑上,則此註解會將應用程式標記為 Web 應用程式,並啟動關鍵行為,例如設定 DispatcherServlet

  • @ComponentScan:告知 Spring 在 com/example 套件中尋找其他元件、設定和服務,使其找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法來啟動應用程式。您是否注意到沒有一行 XML 程式碼?也沒有 web.xml 檔案。此 Web 應用程式是 100% 純 Java,您不必處理設定任何基礎架構。

建置可執行 JAR 檔

您可以使用 Gradle 或 Maven 從命令列執行應用程式。您也可以建置一個包含所有必要相依性、類別和資源的單一可執行 JAR 檔案並執行它。建置可執行 JAR 檔可以輕鬆地在整個開發生命週期、跨不同環境等情況下傳送、版本控制和部署服務作為應用程式。

如果您使用 Gradle,您可以使用 ./gradlew bootRun 執行應用程式。或者,您可以使用 ./gradlew build 建置 JAR 檔案,然後執行 JAR 檔案,如下所示

java -jar build/libs/gs-rest-service-cors-0.1.0.jar

如果您使用 Maven,您可以使用 ./mvnw spring-boot:run 執行應用程式。或者,您可以使用 ./mvnw clean package 建置 JAR 檔案,然後執行 JAR 檔案,如下所示

java -jar target/gs-rest-service-cors-0.1.0.jar
此處描述的步驟會建立可執行的 JAR。您也可以建置傳統 WAR 檔案

會顯示記錄輸出。服務應在幾秒鐘內啟動並執行。

測試服務

現在服務已啟動,請在您的瀏覽器中造訪 https://127.0.0.1:8080/greeting,您應該會看到

{"id":1,"content":"Hello, World!"}

透過造訪 https://127.0.0.1:8080/greeting?name=User 來提供 name 查詢字串參數。content 屬性的值從 Hello, World! 變更為 Hello User!,如下列清單所示

{"id":2,"content":"Hello, User!"}

此變更示範了 GreetingController 中的 @RequestParam 配置如預期般運作。name 參數已給定預設值 World,但始終可以透過查詢字串明確覆寫。

此外,id 屬性已從 1 變更為 2。這證明您正在針對跨多個請求的同一個 GreetingController 實例工作,並且其 counter 欄位正在按預期在每次呼叫時遞增。

現在您可以測試 CORS 標頭是否已就位,並允許來自另一個來源的 Javascript 用戶端存取服務。為此,您需要建立一個 Javascript 用戶端來取用服務。下列清單顯示了這樣一個用戶端

首先,建立一個名為 hello.js 的簡單 Javascript 檔案 (來自 complete/public/hello.js),其內容如下

$(document).ready(function() {
    $.ajax({
        url: "https://127.0.0.1:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

此腳本使用 jQuery 來取用 https://127.0.0.1:8080/greeting 的 REST 服務。它由 index.html 載入,如下列清單 (來自 complete/public/index.html) 所示

<!DOCTYPE html>
<html>
    <head>
        <title>Hello CORS</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="hello.js"></script>
    </head>

    <body>
        <div>
            <p class="greeting-id">The ID is </p>
            <p class="greeting-content">The content is </p>
        </div>
    </body>
</html>

為了測試 CORS 行為,您需要從另一個伺服器或連接埠啟動用戶端。這樣做不僅可以避免兩個應用程式之間的衝突,還可以確保用戶端程式碼是從與服務不同的來源提供的。

若要啟動在連接埠 9000 的 localhost 上執行的用戶端,請保持應用程式在連接埠 8080 上執行,並在另一個終端機中執行下列 Maven 命令

./mvnw spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9000'

如果您使用 Gradle,您可以使用此命令

./gradlew bootRun --args="--server.port=9000"

應用程式啟動後,在您的瀏覽器中開啟 https://127.0.0.1:9000,您應該會看到以下內容,因為服務回應包含相關的 CORS 標頭,因此 ID 和內容會呈現到頁面中

Model data retrieved from the REST service is rendered into the DOM if the proper CORS headers are in the response.

現在,停止在連接埠 9000 上執行的應用程式,保持應用程式在連接埠 8080 上執行,並在另一個終端機中執行下列 Maven 命令

./mvnw spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9001'

如果您使用 Gradle,您可以使用此命令

./gradlew bootRun --args="--server.port=9001"

應用程式啟動後,在您的瀏覽器中開啟 https://127.0.0.1:9001,您應該會看到以下內容

The browser will fail the request if the CORS headers are missing (or insufficient for theclient) from the response. No data will be rendered into the DOM.

在這裡,瀏覽器請求失敗,並且值未呈現到 DOM 中,因為 CORS 標頭遺失 (或對於用戶端而言不足),因為我們僅允許來自 https://127.0.0.1:9000 而不是 https://127.0.0.1:9001 的跨來源請求。

總結

恭喜!您剛剛開發了一個包含 Spring 的跨來源資源共享的 RESTful Web 服務。

取得程式碼