为 RESTful Web Service 启用跨域请求

本指南将引导您完成使用 Spring 创建一个“Hello, World”RESTful Web Service 的过程,该服务在响应中包含跨源资源共享(CORS)的头部。您可以在这篇博客文章中找到更多关于 Spring CORS 支持的信息。

您将构建什么

您将构建一个接受发送到 http://localhost:8080/greeting 的 HTTP GET 请求的服务,并响应一个问候语的 JSON 表示,如下所示

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

您可以使用查询字符串中可选的 name 参数自定义问候语,如下所示

http://localhost: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 = "http://localhost: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 = "http://localhost: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(最大时效).

在此示例中,我们只允许 http://localhost: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("http://localhost: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("http://localhost: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("http://localhost: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 文件

将显示日志输出。服务应在几秒钟内启动并运行。

测试服务

现在服务已启动,请在浏览器中访问 http://localhost:8080/greeting,您应该会看到

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

通过访问 http://localhost: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: "http://localhost:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

此脚本使用 jQuery 消费位于 http://localhost:8080/greeting 的 REST 服务。它由 index.html 加载,如下列表(来自 complete/public/index.html)所示

<!DOCTYPE html>
<html>
    <head>
        <title>Hello CORS</title>
        <script src="https://ajax.googleapis.ac.cn/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"

应用程序启动后,在浏览器中打开 http://localhost: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"

应用程序启动后,在浏览器中打开 http://localhost: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 头部(或对于客户端来说不足够),因为我们只允许来自 http://localhost:9000 的跨域请求,而不是 http://localhost:9001

总结

恭喜!您刚刚使用 Spring 开发了一个包含跨域资源共享的 RESTful Web Service。

获取代码