为 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 开始

您可以使用此预初始化项目并点击生成以下载 ZIP 文件。此项目已配置为适合本教程中的示例。

要手动初始化项目

  1. 导航到https://start.spring.io。此服务会引入应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您要使用的语言。本指南假设您选择了 Java。

  3. 点击依赖项并选择Spring Web

  4. 点击生成

  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 通过返回 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://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。如果请求中不存在,则使用 defaultValueWorld

方法体实现创建并返回一个新的 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

总结

恭喜!您刚刚开发了一个包含跨源资源共享的 RESTful Web 服务。

获取代码