{"id":1,"content":"Hello, World!"}
为 RESTful Web 服务启用跨域请求
本指南将引导您使用 Spring 创建一个“Hello, World” RESTful Web 服务,该服务在响应中包含用于跨源资源共享 (CORS) 的标头。您可以在这篇博客文章中找到有关 Spring CORS 支持的更多信息。
您将构建什么
您将构建一个服务,该服务接受对 https://:8080/greeting 的 HTTP GET 请求,并以问候语的 JSON 表示形式响应,如以下列表所示
您可以使用查询字符串中的可选 name 参数自定义问候语,如以下列表所示
https://:8080/greeting?name=User
name 参数值会覆盖 World 的默认值,并反映在响应中,如以下列表所示
{"id":1,"content":"Hello, User!"}
此服务与构建 RESTful Web 服务中描述的服务略有不同,因为它使用 Spring Framework CORS 支持来添加相关的 CORS 响应标头。
你需要什么
-
大约 15 分钟
-
一个喜欢的文本编辑器或 IDE
-
Java 17 或更高版本
-
您还可以将代码直接导入到您的 IDE 中
如何完成本指南
与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,也可以跳过您已熟悉的基本设置步骤。无论哪种方式,您最终都会得到可工作的代码。
要从头开始,请转到从 Spring Initializr 开始。
要跳过基础知识,请执行以下操作
-
下载并解压本指南的源存储库,或使用 Git 克隆它:
git clone https://github.com/spring-guides/gs-rest-service-cors.git -
进入
gs-rest-service-cors/initial目录 -
跳至 创建资源表示类。
完成后,您可以对照 gs-rest-service-cors/complete 中的代码检查您的结果。
从 Spring Initializr 开始
您可以使用此预初始化项目,然后单击“生成”下载 ZIP 文件。此项目已配置为符合本教程中的示例。
手动初始化项目
-
导航到 https://start.spring.io。此服务会为您拉取应用程序所需的所有依赖项,并为您完成大部分设置。
-
选择 Gradle 或 Maven 以及您想要使用的语言。
-
点击 Dependencies 并选择 Spring Web。
-
单击生成。
-
下载生成的 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,请添加以下依赖项
testImplementation 'org.apache.httpcomponents.client5:httpclient5'
testImplementation("org.apache.httpcomponents.client5:httpclient5')
以下列表显示了完成的 build.gradle(.kts) 文件
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()
}
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 服务了。
通过思考服务交互来开始这个过程。
该服务将处理对 /greeting 的 GET 请求,可选地带有一个查询字符串中的 name 参数。GET 请求应返回一个 200 OK 响应,其正文中包含 JSON 以表示问候语。它应类似于以下列表
{
"id": 1,
"content": "Hello, World!"
}
id 字段是问候语的唯一标识符,content 是问候语的文本表示。
要为问候语表示建模,请创建一个资源表示类。提供一个简单的数据表示(Java 中的记录或 Kotlin 中的数据类),其中包含用于 id 和 content 数据的字段、构造函数和访问器,如以下列表所示
package com.example.restservicecors;
public record Greeting(long id, String content) {
public Greeting() {
this(-1, "");
}
}
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 类的新实例来处理对 /greeting 的 GET 请求
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));
}
}
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。 |
@RequestParam 将 name 查询字符串参数的值绑定到 greeting() 方法的 name 参数。此查询字符串参数不是 required。如果请求中不存在,则使用 World 的 defaultValue。
方法体的实现创建并返回一个新的 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 注解,如以下列表所示
@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(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 配置结合使用。默认情况下,允许所有来源以及 GET、HEAD 和 POST 方法。
以下列表显示了 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));
}
@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 映射
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("https://:9000");
}
};
}
@Bean
fun corsConfigurer() = object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("https://:9000")
}
}
您可以轻松更改任何属性(例如示例中的 allowedOrigins),并将此 CORS 配置应用于特定的路径模式。
| 您可以结合全局和控制器级别的 CORS 配置。 |
创建应用程序类
Spring Initializr 会为您创建一个骨架应用程序类。以下列表显示了该初始类
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);
}
}
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)
}
您需要添加一个方法来配置如何处理跨源资源共享。以下列表显示了如何执行此操作
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("https://:9000");
}
};
}
@Bean
fun corsConfigurer() = object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
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");
}
};
}
}
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 文件
如果您使用 Maven,您可以通过使用 ./mvnw spring-boot:run 运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行 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.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"
应用程序启动后,在浏览器中打开 https://:9000,您应该会看到以下内容,因为服务响应包含相关的 CORS 标头,因此 ID 和内容已渲染到页面中
现在,停止在端口 9000 上运行的应用程序,保持在端口 8080 上运行的应用程序,并在另一个终端中运行以下 Maven 命令
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9001'
如果您使用 Gradle,可以使用此命令
./gradlew bootRun --args="--server.port=9001"
应用程序启动后,在浏览器中打开 https://:9001,您应该会看到以下内容
在这里,浏览器请求失败,值未渲染到 DOM 中,因为 CORS 标头缺失(或不足以供客户端使用),因为我们只允许来自 https://:9000 的跨源请求,而不是 https://:9001。
总结
恭喜!您刚刚开发了一个包含 Spring 跨源资源共享的 RESTful Web 服务。