SpringBoot 2 为 Web 服务开启跨源请求(CORS)

开篇词

该指南将引导你使用 Spring 创建 “Hello, World” RESTful Web服务的过程,该服务在响应中包括跨域资源共享(CORS)的标头。你可以在该博客文章中找到有关 Spring CORS 支持的更多信息。
 

你将创建的应用

我们将创建一个具有静态主页的应用,该应用还将在以下地址接受 HTTP GET 请求:http://localhost:8080/greeting 并以问候语的 JSON 表示形式进行响应,如下清单所示:

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

我们可以在查询字符串中使用可选的 name 参数来自定义问候语,如下清单所示:

http://localhost:8080/greeting?name=User

name 参数值将覆盖 World 的默认值,并且在内容中更改为 “Hello, User!” 会反映在响应中,如下清单所示:

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

该服务与构建 RESTful Web 服务中描述的服务略有不同,因为它使用 Spring 框架的 CORS 支持来添加相关的 CORS 响应标头。
 

你将需要的工具

如何完成这个指南

像大多数的 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 开始

对于所有的 Spring 应用来说,你应该从 Spring Initializr 开始。Initializr 提供了一种快速的方法来提取应用程序所需的依赖,并为你完成许多设置。该示例仅需要 Spring Web 依赖。下图显示了此示例项目的 Initializr 设置:
Spring Initializr 界面

上图显示了选择 Maven 作为构建工具的 Initializr。你也可以使用 Gradle。它还将 com.examplerest-service-cors 的值分别显示为 Group 和 Artifact。在本示例的其余部分,将用到这些值。

以下清单显示了选择 Maven 时创建的 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>2.2.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>rest-service-cors</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>rest-service-cors</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

以下清单显示了在选择 Gradle 时创建的 build.gradle 文件:

扫描二维码关注公众号,回复: 9499512 查看本文章
plugins {
	id 'org.springframework.boot' version '2.2.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.8.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

添加 httpClient 依赖

测试(在 complete/src/test/java/com/example/restservicecors/GreetingIntegrationTests.java)需要 Apache httpclient 库。
要将 Apache httpclient 库添加到 Maven,请添加以下依赖:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</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>2.2.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>rest-service-cors</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>rest-service-cors</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

要将 Apache httpclient 库添加到 Gradle,请添加以下依赖:

testImplementation 'org.apache.httpcomponents:httpclient'

以下清单显示了完整的 build.gradle 文件:

plugins {
	id 'org.springframework.boot' version '2.2.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.8.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.apache.httpcomponents:httpclient'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

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 网络服务的方法中,HTTP 请求由控制器处理。这些组件可以通过 @Controller 注解轻松识别,并且以下清单中所示的 GreetingContoller(来自 src/main/java/com/example/restservicecors/GreetingController.java)通过返回 Greeting 类的新实例来处理 /greetingGET 请求:

package com.example.restservicecors;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.CrossOrigin;
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("==== in greeting ====");
		return new Greeting(counter.incrementAndGet(), String.format(template, name));
	}

}

该控制器简洁明了,但是内部却有很多事情要做。我们将其逐步分解。

@RequestMapping 注解可确保将对 /greeting 的 HTTP 请求映射到 greeting() 方法。

前面的示例使用 @GetMapping 注解,该注解用作 @RequestMapping(method=RequestMethod.GET) 的快捷方式。

@RequestParamname 查询字符串参数的值绑定到 greeting() 方法的 name 参数中。不 required 该查询字符串参数。如果请求中不存在,则使用 defaultValueWorld

方法主体的实现创建并返回一个新的 Greeting 对象,其 id 属性的值基于来自 counter 的下一个值,而 content 的基于查询参数或默认值。它还使用问候 template 格式化给定 name

传统 MVC 控制器和 RESTful Web 服务控制器之间的主要区别在于创建 HTTP 响应主体的方式。该 RESTful Web 服务控制器填充并返回 Greeting 对象,而不是依赖于视图技术来将问候数据执行到 HTML 的服务器端渲染。对象数据作为 JSON 直接写入 HTTP 响应。

为了实现这一点,greeting() 方法上的 @ResponseBody 注解告诉 Spring MVC,它不需要通过服务器端视图层来渲染 Greeting 对象。相反,返回的问候语对象是响应主体,应直接写出。

Greeting 对象必须转换为 JSON。借助 Spring 的 HTTP 消息转换器支持,我们无需手动进行该转换。由于 Jackson 位于类路径中,因此会自动选择 Spring 的 MappingJackson2HttpMessageConverter 来将 Greeting 实例转换为 JSON。
 

启用 CORS

我们可以从单个控制器或全局控制器中启用跨域资源共享(CORS)。以下主题描述了如何执行该操作:

  • 控制器方法 CORS 配置
  • 全局 CORS 配置

控制器方法 CORS 配置

为了使 RESTful Web 服务的响应中包含 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("==== in greeting ====");
		return new Greeting(counter.incrementAndGet(), String.format(template, name));
	}

@CrossOrigin 注解仅针对该特定方法启用跨域资源共享。默认情况下,它允许 @RequestMapping 注解中指定的所有来源,所有标头和 HTTP 方法。另外,使用 30 分钟的 maxAge。我们可以通过指定以下注解属性之一的值来定义该行为:

  • origins
  • 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。
  • @ComponentScan:告知 Spring 在 com/example 包中寻找他组件、配置以及服务。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法启动应用。
 

构建可执行 JAR

我们可以结合 Gradle 或 Maven 来从命令行运行该应用。我们还可以构建一个包含所有必须依赖项、类以及资源的可执行 JAR 文件,然后运行该文件。在整个开发生命周期中,跨环境等等情况下,构建可执行 JAR 可以轻松地将服务作为应用进行发布、版本化以及部署。

如果使用 Gradle,则可以借助 ./gradlew bootRun 来运行应用。或通过借助 ./gradlew build 来构建 JAR 文件,然后运行 JAR 文件,如下所示:

java -jar build/libs/gs-rest-service-cors-0.1.0.jar

由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:java -jar build/libs/rest-service-cors-0.0.1-SNAPSHOT.jar

如果使用 Maven,则可以借助 ./mvnw spring-boot:run 来运行该用。或可以借助 ./mvnw clean package 来构建 JAR 文件,然后运行 JAR 文件,如下所示:

java -jar target/gs-rest-service-cors-0.1.0.jar

由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:java -jar target/rest-service-cors-0.0.1-SNAPSHOT.jar

我们还可以将 JAR 应用转换成 WAR 应用

显示日志记录输出。该服务应在几秒内启动并运行。
 

测试服务

现在该服务已启动,请访问 http://localhost:8080/greeting,我们应在其中看到:

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

通过访问 http://localhost:8080/greeting?name=User 提供名称查询字符串参数。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.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>

从本质上讲,这是在使用 jQuery 的 RESTful Web 服务中创建的 REST 客户端,当它在 localhost 的端口 8080 上运行时,对其进行了少许修改以使用该服务。有关如何开发该客户端的更多详细信息,请参见该指南。

由于 REST 服务已经在本地主机的 8080 端口上运行,因此需要确保从其他服务器或端口启动客户端。这样做不仅避免了两个应用之间的冲突,而且还要确保客户端代码与服务所处的源不同。要在 9000 端口上启动并运行客户端,请运行以下 Maven 命令:

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

由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:java -jar -Dserver.port=9000 build/libs/rest-service-cors-0.0.1-SNAPSHOT.jar

客户端启动后,在浏览器中访问 http://localhost:9000,我们将在其中看到以下内容:

如果服务响应包含 CORS 标头,则 ID 和 content 将呈现到页面中。但是,如果缺少 CORS 标头(或为为客户端进行充分的定义),则浏览器将请求失败,并且不会将值呈现到 DOM 中。在这种情况下,我们应该看到以下内容:

概述

恭喜你!我们刚刚开发了一个 RESTful Web 服务,其中包括与 Spring 的跨域资源共享。
 

参见

以下指南也可能会有所帮助:

想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南

发布了182 篇原创文章 · 获赞 12 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/stevenchen1989/article/details/104422035