為 RESTful Web Service 啟用跨域請求

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

您將構建什麼

您將構建一個接受傳送到 https://:8080/greeting 的 HTTP GET 請求的服務,並響應一個問候語的 JSON 表示,如下所示

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

您可以使用查詢字串中可選的 name 引數自定義問候語,如下所示

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

name 引數的值會覆蓋預設值 World,並反映在響應中,如下所示

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

此服務與構建 RESTful Web Service 中描述的服務略有不同,它使用了 Spring Framework 的 CORS 支援來新增相關的 CORS 響應頭部。

您需要準備什麼

如何完成本指南

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

從頭開始,請繼續閱讀使用 Spring Initializr 開始

跳過基礎部分,請執行以下操作

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

使用 Spring Initializr 開始

您可以使用此預初始化專案,然後單擊“生成”下載 ZIP 檔案。此專案已配置,適用於本教程中的示例。

手動初始化專案

  1. 導航到 https://start.spring.io。此服務將拉取應用程式所需的所有依賴項,併為您完成大部分設定。

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

  3. 點選Dependencies(依賴項),然後選擇Spring Web

  4. 點選Generate(生成)。

  5. 下載生成的 ZIP 檔案,它是根據您的選擇配置的 Web 應用程式存檔。

如果您的 IDE 集成了 Spring Initializr,您可以在 IDE 中完成此過程。
您也可以從 Github fork 專案並在您的 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 Service 了。

透過思考服務互動開始這個過程。

該服務將處理傳送到 /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 Service 的方法中,HTTP 請求由控制器處理。這些元件很容易透過 @Controller 註解來識別,並且下面的列表(來自 src/main/java/com/example/restservicecors/GreetingController.java)中顯示的 GreetingController 透過返回一個新的 Greeting 類例項來處理傳送到 /greetingGET 請求

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://: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 是因為它便於測試。Spring 仍然會拒絕來源與 CORS 配置不匹配的 GET 請求。瀏覽器不需要傳送 CORS 預檢請求,但如果我們想觸發預檢,可以使用 @PostMapping 並在請求主體中接受一些 JSON。

@RequestParamname 查詢字串引數的值繫結到 greeting() 方法的 name 引數。此查詢字串引數不是 required(必需)的。如果請求中缺少它,則使用 defaultValue(預設值)World

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

傳統 MVC 控制器與前面所示的 RESTful Web Service 控制器之間的關鍵區別在於建立 HTTP 響應主體的方式。RESTful Web Service 控制器不是依賴檢視技術在伺服器端將問候語資料渲染為 HTML,而是填充並返回一個 Greeting 物件。物件資料直接作為 JSON 寫入 HTTP 響應。

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

得益於 Spring 的 HTTP 訊息轉換器支援,Greeting 物件自然地被轉換為 JSON。由於 Jackson 位於類路徑中,Spring 的 MappingJackson2HttpMessageConverter 會自動被選擇來將 Greeting 例項轉換為 JSON。

啟用 CORS

您可以從單個控制器或全域性啟用跨域資源共享(CORS)。以下主題描述瞭如何進行

控制器方法 CORS 配置

為了讓 RESTful Web Service 在其響應中包含 CORS 訪問控制頭部,您必須向處理程式方法新增一個 @CrossOrigin 註解,如下列表(來自 src/main/java/com/example/restservicecors/GreetingController.java)所示

	@CrossOrigin(origins = "https://: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://: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://: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://: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://: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://:8080/greeting,您應該會看到

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

透過訪問 https://: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://:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

此指令碼使用 jQuery 消費位於 https://: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 上啟動客戶端,請保持應用程式在埠 8080 上執行,並在另一個終端中執行以下 Maven 命令

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

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

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

應用程式啟動後,在瀏覽器中開啟 https://:9000,您應該會看到以下內容,因為服務響應包含了相關的 CORS 頭部,所以 ID 和 content 被渲染到頁面中

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://: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://:9000 的跨域請求,而不是 https://:9001

總結

恭喜!您剛剛使用 Spring 開發了一個包含跨域資源共享的 RESTful Web Service。

獲取程式碼