為 RESTful Web 服務啟用跨域請求

本指南將引導您使用 Spring 建立一個“Hello, World” RESTful Web 服務,該服務在響應中包含用於跨源資源共享 (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 服務中描述的服務略有不同,因為它使用 Spring Framework CORS 支援來新增相關的 CORS 響應標頭。

你需要什麼

如何完成本指南

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

從頭開始,請轉到從 Spring Initializr 開始

跳過基礎知識,請執行以下操作

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

從 Spring Initializr 開始

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

手動初始化專案

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

  2. 選擇 Gradle 或 Maven 以及您想要使用的語言。

  3. 點選 Dependencies 並選擇 Spring Web

  4. 單擊生成

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

如果您的 IDE 集成了 Spring Initializr,您可以從 IDE 中完成此過程。
您還可以從 GitHub fork 專案,並在您的 IDE 或其他編輯器中開啟它。

新增 httpclient5 依賴項

測試需要 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.5.7</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,請新增以下依賴項

Groovy (build.gradle)
testImplementation 'org.apache.httpcomponents.client5:httpclient5'
Kotlin (build.gradle.kts)
testImplementation("org.apache.httpcomponents.client5:httpclient5')

以下列表顯示了完成的 build.gradle(.kts) 檔案

Groovy (build.gradle)
plugins {
	id 'org.springframework.boot' version '3.5.7'
	id 'io.spring.dependency-management' version '1.1.7'
	id 'java'
}

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()
}
Kotlin (build.gradle.kts)
plugins {
    id("org.springframework.boot") version "3.5.7"
    id("io.spring.dependency-management") version "1.1.7"
    kotlin("jvm") version "1.9.25"
    kotlin("plugin.spring") version "1.9.25"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    testImplementation("org.apache.httpcomponents.client5:httpclient5")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

kotlin {
    jvmToolchain(17)
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}


tasks.withType<Test> {
    useJUnitPlatform()
}

建立資源表示類

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

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

該服務將處理對 /greetingGET 請求,可選地帶有一個查詢字串中的 name 引數。GET 請求應返回一個 200 OK 響應,其正文中包含 JSON 以表示問候語。它應類似於以下列表

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

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

要為問候語表示建模,請建立一個資源表示類。提供一個簡單的資料表示(Java 中的記錄或 Kotlin 中的資料類),其中包含用於 idcontent 資料的欄位、建構函式和訪問器,如以下列表所示

Java
package com.example.restservicecors;

public record Greeting(long id, String content) {

	public Greeting() {
		this(-1, "");
	}
}
Kotlin
package com.example.restservicecors

data class Greeting(
    val id: Long = -1,
    val content: String = ""
)
Spring 使用 Jackson JSON 庫自動將 Greeting 型別的例項序列化為 JSON。

建立資源控制器

在 Spring 構建 RESTful Web 服務的方法中,HTTP 請求由控制器處理。這些元件很容易透過 @Controller 註解來識別,如下列表所示的 GreetingController 透過返回 Greeting 類的新例項來處理對 /greetingGET 請求

Java
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();

	@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));
	}

}
Kotlin
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
class GreetingController {

    private val counter = AtomicLong()

    @GetMapping("/greeting")
    fun greeting(@RequestParam(required = false, defaultValue = "World") name: String): Greeting {
        println("==== get greeting ====")
        return Greeting(id = counter.incrementAndGet(), content = "Hello, $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 物件。物件資料直接作為 JSON 寫入 HTTP 響應。

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

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

啟用 CORS

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

控制器方法 CORS 配置

為了使 RESTful Web 服務在其響應中包含 CORS 訪問控制標頭,您必須向處理程式方法新增 @CrossOrigin 註解,如以下列表所示

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));
}
Kotlin
@CrossOrigin(origins = ["https://:9000"])
@GetMapping("/greeting")
fun greeting(@RequestParam(required = false, defaultValue = "World") name: String): Greeting {
    println("==== get greeting ====")
    return Greeting(id = counter.incrementAndGet(), content = "Hello, $name!")
}

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

  • 來源 (origins)

  • 來源模式 (originPatterns)

  • 方法 (methods)

  • 允許的標頭 (allowedHeaders)

  • 暴露的標頭 (exposedHeaders)

  • 允許憑據 (allowCredentials)

  • 最大年齡 (maxAge).

在此示例中,我們只允許 https://:9000 傳送跨源請求。

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

全域性 CORS 配置

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

以下列表顯示了 GreetingController 類中的 greetingWithJavaconfig 方法

Java
@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));
}
Kotlin
@GetMapping("/greeting-javaconfig")
fun greetingWithJavaconfig(@RequestParam(required = false, defaultValue = "World") name: String): Greeting {
    println("==== in greeting ====")
    return Greeting(id = counter.incrementAndGet(), content = "Hello, $name!")
}
greetingWithJavaconfig 方法和 greeting 方法(用於控制器級 CORS 配置)之間的區別在於路由(/greeting-javaconfig 而不是 /greeting)以及 @CrossOrigin 源的存在。

以下列表顯示瞭如何在應用程式類中新增 CORS 對映

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

您可以輕鬆更改任何屬性(例如示例中的 allowedOrigins),並將此 CORS 配置應用於特定的路徑模式。

您可以結合全域性和控制器級別的 CORS 配置。

建立應用程式類

Spring Initializr 會為您建立一個骨架應用程式類。以下列表顯示了該初始類

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);
	}

}
Kotlin
package com.example.restservicecors

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class RestServiceCorsApplication

fun main(args: Array<String>) {
    runApplication<RestServiceCorsApplication>(*args)
}

您需要新增一個方法來配置如何處理跨源資源共享。以下列表顯示瞭如何執行此操作

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

以下列表顯示了完成的應用程式類

Java
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");
			}
		};
	}
}
Kotlin
package com.example.restservicecors

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

@SpringBootApplication
class RestServiceCorsApplication {

    @Bean
    fun corsConfigurer() = object : WebMvcConfigurer {
        override fun addCorsMappings(registry: CorsRegistry) {
            registry.addMapping("/greeting-javaconfig").allowedOrigins("https://:9000")
        }
    }
}

fun main(args: Array<String>) {
    runApplication<RestServiceCorsApplication>(*args)
}

@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 檔案(來自 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 載入,如以下列表所示(來自 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 和內容已渲染到頁面中

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 服務。

獲取程式碼