SpringBoot2-高级教程-三-

128 阅读31分钟

SpringBoot2 高级教程(三)

原文:Pro Spring Boot 2

协议:CC BY-NC-SA 4.0

六、Spring Boot 的网络流量和反应数据

在这一章中,我将向你展示 Spring Framework 5 的最新补充,以及如何在 Spring Boot 中使用它。为 web 应用构建的新的反应器栈是 Spring WebFlux,它是在 Spring 框架的 5.0 版本中添加的。它是一个完全非阻塞的框架,依赖于 Project Reactor,支持 reactive streams back pressure,运行在 Netty 和 Undertow 等服务器以及 Servlet 3.1+容器上。

在我向你展示如何在 Spring Boot 中使用 WebFlux 之前,让我们先了解一下反应式系统以及 Project Reactor ( https://projectreactor.io/ )是如何实现它们的。

反应系统

在过去的十年里,我们参与了软件的变革,让软件变得更稳定、更健壮、更有弹性、更灵活,以接受更现代的需求,不仅是用户(使用桌面或网络),还有许多设备(手机、传感器等)。).接受这些新的工作负载面临许多挑战;这就是为什么一组组织共同努力,带来一份清单,以涵盖当今数据需求的许多方面。

反动宣言

《反动宣言》( https://www.reactivemanifesto.org/ )签署于 2014 年 9 月 16 日。它定义了反应系统应该如何。反应式系统是灵活的、松散耦合的和可伸缩的。这些系统更能容忍失败,当失败发生时,它们通过应用模式来处理它,以避免灾难。这些反应系统已经定义了某些特征。

反应系统是

  • 反应灵敏。如果可能的话,大多数系统都会及时响应;他们专注于提供快速一致的响应时间,并可靠地交付一致的服务质量。

  • 富有弹性。他们应用复制、包容、隔离和委托模式来提供弹性系统。系统的故障必须通过隔离来控制;故障不应影响其他系统。必须从另一个系统恢复,以确保高可用性(HA)。

  • 弹性。系统必须对任何类型的工作负载做出响应。反应式系统可以通过增加或减少分配给这些输入的资源来对输入速率的变化做出反应。不应该有任何瓶颈,这意味着系统有能力共享或复制组件。反应式系统必须支持预测算法,以确保商用硬件的成本效益弹性。

  • 消息驱动。反应式系统必须依赖异步消息传递来建立组件之间的边界,确保系统是松散耦合的、隔离的和位置透明的。它必须通过在需要时提供背压模式来支持负载管理、弹性和流量控制。通信必须是非阻塞的,以允许在活动时使用资源,从而降低系统开销。

随着 Reactive Manifesto 的出现,不同的计划开始出现并实现框架和库,帮助世界各地的许多开发人员。Reactive Streams ( www.reactive-streams.org )是一个规范,定义了四个简单的接口(Publisher<T>,一个无限数量有序元素的提供者,根据订阅者的需求发布它们;Subscriber<T>订阅出版商;Subscription表示订阅发布者的订阅者一对一的生命周期;和Processor,这是对SubscriberPublisher以及不同实现的处理阶段,如 react vex rx Java(http://reactivex.io/)、Akka Streams ( https://akka.io/ )、Ratpack ( https://ratpack.io/ )、Vert。X ( https://vertx.io/ )、Slick、Project Reactor 等等。

在 Java 9 SDK 版本中,Reactive Streams API 有自己的实现;换句话说,截至 2017 年 12 月,Reactive Streams 1 . 0 . 2 版是 JDK9 的一部分。

项目反应器

Project Reactor 3.x 是一个围绕 Reactive Streams 规范构建的库,为 JVM 带来了反应式编程范例。反应式编程是一种基于事件模型的范例,其中数据在变得可用时被推送给消费者;它处理异步事件序列。反应式编程提供了完全异步和非阻塞的模式,是在 JDK(回调、API 和未来< V > 接口)中执行异步代码的有限方式的替代方案。

Reactor 是一个完整的、非阻塞的反应式编程框架,它管理背压并集成了与 Java 8 功能 API(CompletableFutureStreamDuration)的交互。Reactor 提供了两个反应式的可组合异步 APIsFlux N,以及 Mono 0|1。Reactor 可以用于开发微服务架构,因为它提供了带有 reactor-ipc 组件的 IPC(进程间通信)和用于 HTTP(包括 WebSockets、TCP 和 UDP)的背压就绪网络引擎,并且完全支持反应式编码和解码。

Project Reactor 提供了处理器、操作器和计时器,可以在低内存占用的情况下保持每秒数千万条消息的高吞吐率。

注意

如果您想了解更多关于 Project Reactor 的信息,请访问 https://projectreactor.io/ 及其在 http://projectreactor.io/docs/core/release/reference/docs/index.html 的文档。

带有反应器的待办事项应用

让我们开始在 ToDo 应用中使用 Reactor,并尝试 Flux 和 Mono APIs。在本节中,创建一个简单的 Reactor 示例来处理 ToDo。

打开自己喜欢的浏览器,指向 Spring Initializr(https://start.spring.io);将以下值添加到字段中。

  • 组:com.apress.reactor

  • 神器:example

  • 依赖关系:Lombok

您可以选择 Maven 或 Gradle 作为项目类型。然后点击 Generate Project 按钮,下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 6-1 )。

img/340891_2_En_6_Fig1_HTML.jpg

图 6-1

spring initializehttps://start.spring.io

如您所见,这次在龙目语( https://projectlombok.org/ )中没有明显的依赖关系。我们会尽可能简单地解决这个问题。这只是 Flux 和 Mono APIs 的一点点味道。稍后,您将使用 WebFlux 框架创建一个 web 应用。

如果您正在使用 Maven,请打开您的pom.xml并添加以下部分和依赖项。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-bom</artifactId>
            <version>Bismuth-SR10</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
      <!-- ... more dependencies here ... -->

      <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
      </dependency>

</dependencies>

Reactor 对reactive-streamsjar 有一个可传递的依赖,所以通过添加物料清单(BOM), Project Reactor 提供了所有必需的 jar。

如果您使用的是 Gradle,将下面的部分和依赖项添加到您的build.gradle文件中。

dependencyManagement {
    imports {
        mavenBom "io.projectreactor:reactor-bom:Bismuth-SR10"
    }
}

dependencies {
      // ... More dependencies here ...
      compile('io.projectreactor:reactor-core')
}

接下来,让我们创建ToDo域类,但是这一次,它没有持久性(参见清单 6-1 )。

package com.apress.reactor.example.domain;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-1com.apress.reactor.example.domain.ToDo.java

清单 6-1 显示了ToDo类。这个类没有什么特别的,但是我们一直在使用持久技术;在这种情况下,您可以让它保持原样—简单。让我们从定义 Mono 和 Flux 反应 API 开始,并添加必要的代码来使用ToDo域类。

Mono ,异步[0|1]结果

Mono<T>是发射一个项目的专用Publisher<T>,它可以选择终止于onCompleteonError信号。您可以应用操作符来操作该项目(参见图 6-2 )。

img/340891_2_En_6_Fig2_HTML.jpg

图 6-2

Mono 0|1 文档)。

接下来,让我们创建一个MonoExample类,并学习如何使用 Mono API(参见清单 6-2 )。

package com.apress.reactor.example;

import com.apress.reactor.example.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.scheduler.Schedulers;

import java.time.Duration;

@Configuration
public class MonoExample {

    static private Logger LOG = LoggerFactory.getLogger(MonoExample.class);

    @Bean
    public CommandLineRunner runMonoExample(){
        return args -> {

       MonoProcessor<ToDo> promise = MonoProcessor.create();
       Mono<ToDo> result = promise
            .doOnSuccess(p -> LOG.info("MONO >> ToDo: {}", p.getDescription()))
            .doOnTerminate( () -> LOG.info("MONO >> Done"))
            .doOnError(t -> LOG.error(t.getMessage(), t))
            .subscribeOn(Schedulers.single());

            promise.onNext(
new ToDo("Buy my ticket for SpringOne Platform 2018"));
          //promise.onError(
new IllegalArgumentException("There is an error processing the ToDo..."));

            result.block(Duration.ofMillis(1000));
        };
    }
}

Listing 6-2com.apress.reactor.example.MonoExample.java

清单 6-2 显示了MonoExample类;我们来分析一下。

  • MonoProcessor。在 Reactor 中,有些处理器既是发布者,也是订阅者;这意味着您可以订阅处理器,但也可以调用方法来手动将数据注入序列或终止序列。在本例中,您使用了onNext方法来发出一个ToDo实例。

  • Mono。这是一个带有基本运算符的反应式流发布器,通过发出元素或出错来成功完成。

  • doOnSuccess。当Mono成功完成时,调用或触发该方法。

  • doOnTerminate。当Mono因成功完成或出错而终止时,调用或触发该方法。

  • doOnError。当Mono类型完成时出现错误,这个方法被调用。

  • subscribeOn。订阅Mono类型并请求对指定的Scheduler工作线程的无限制需求。

  • onNext。这个方法发出一个可以标记为@Nullable的值。

  • block。订阅Mono类型并阻塞,直到接收到下一个信号或超时。

你可以看到一个非常简单的例子,但请记住,现在我们讨论的是反应式编程,不再有来自服务器的阻塞或轮询,而是推送到消费者,直到它发回一个完成的信号。这使得我们的应用更加高效和健壮。我们可以说我们再也不能拥有迟钝的消费者了。

现在,您可以使用命令行或 IDE 来运行该应用。您应该会看到以下输出:

INFO 55588 - [single-1] c.a.r.e.MonoExample   : MONO >> ToDo: Buy my ticket for SpringOne Platform 2018
INFO 55588 - [single-1] c.a.r.e.MonoExample   : MONO >> Done

注意

这段代码是如何运行的?记得我们已经将类标记为@Configuration,并且声明了一个返回CommandLineRunner接口的@Bean。Spring Boot 在完成所有 Spring beans 的连接之后、应用启动之前执行这个 bean;所以这是在应用运行之前执行代码(比如初始化)的好方法。

Flux :一个[0|N]项的异步序列

Flux 是一个代表 0 到 N 个发射项目的异步序列的Publisher<T>,该序列可以通过使用onCompleteonError信号选择性地终止(见图 6-3 )。

img/340891_2_En_6_Fig3_HTML.jpg

图 6-3

Flux 0|N 文档)。

创建FluxExample类(参见清单 6-3 )。

package com.apress.reactor.example;

import com.apress.reactor.example.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.util.List;

@Configuration
public class FluxExample {

    static private Logger LOG = LoggerFactory.getLogger(FluxExample.class);

    @Bean
    public CommandLineRunner runFluxExample(){
        return args -> {

       EmitterProcessor<ToDo> stream =
                        EmitterProcessor.create();

       Mono<List<ToDo>> promise = stream
                     .filter( s -> s.isCompleted())
                     .doOnNext(s -> LOG.info("FLUX >>> ToDo: {}", s.getDescription()))
                   .collectList()
                   .subscribeOn(Schedulers.single());

       stream.onNext(new ToDo("Read a Book",true));
       stream.onNext(new ToDo("Listen Classical Music",true));
       stream.onNext(new ToDo("Workout in the Mornings"));
       stream.onNext(new ToDo("Organize my room", true));
       stream.onNext(new ToDo("Go to the Car Wash", true));
       stream.onNext(new ToDo("SP1 2018 is coming" , true));

            stream.onComplete();

            promise.block();

        };
    }
}

Listing 6-3com.apress.reactor.example.FluxExample.java

清单 6-3 显示了FluxExample类。我们来分析一下。

  • EmitterProcessor。请记住,处理器是一种发布者;在这种情况下,我们使用一个同步处理器,它可以通过用户操作和订阅上游发布者并同步排出数据来推送数据。这个处理器创建了一系列ToDo实例;它提供了一个由RingBuffer支持的消息传递处理器的实现,该处理器实现了带有同步漏循环的publish-subscribe。如果你想使用异步处理器,你可以使用WorkQueueProcessorTopicProcessor

  • filter。请记住,您可以将运算符应用于 Flux 和 Mono APIs 在这种情况下,使用一个谓词来应用过滤器,该谓词评估并在成功时发出一个值(即,如果完成了ToDo)。

  • doOnNext。当通量发出一个项目时触发。

  • collectList。将 flux 发出的所有元素收集到一个列表中,该列表在此序列完成时由结果单声道发出。

  • subscribeOn。基于调度器工作线程订阅此流量。

  • onNext。向通量发出新值。

  • onComplete。完成上游。

  • block。订阅Mono类型并阻塞,直到接收到下一个信号或超时。

现在,您可以运行代码了。您应该得到类似于下面的输出。

INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Read a Book
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Listen Classical Music
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Organize my room
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: Go to the Car Wash
INFO 61 - [single-1] c.a.r.e.FluxExample : FLUX >>> ToDo: SP1 2018 is coming

同样,这是一个简单的 ToDo 应用示例。想象一下,你有数百万用户访问你的应用,每个账户发布 ToDos,你想跟踪他们中的每一个人,就像 Twitter feed 一样,你不想阻止任何用户。你要反应!

webflux

长期以来,Spring MVC 一直是使用 Spring 框架创建 web 应用的主要方式。现在,另一个参与者出现了——反应式堆栈,Spring WebFlux 框架!Spring WebFlux 是一个完全异步和非阻塞的框架,它依赖于 Project Reactor 的 Reactive Streams 实现。

Spring WebFlux 的一个主要特性是它提供了两种编程模型。

  • 带注释的控制器。与 Spring MVC 一致,基于来自spring-web模块的相同注释;这意味着您可以使用相同的已知注释(@*Controller@*Mapping@RequestBody等。)但是有了 Reactor 和 RxJava 的所有反应式支持。

    @RestController
    public class ToDoController {
    
          @GetMapping("/todo/{id}")
          public Mono<ToDo> getToDo(@PathVariable Long id) {
                // ...
          }
    
          @GetMapping("/todo")
          public Flux<ToDo> getToDos() {
                // ...
          }
    }
    
    
  • 功能终点。一个函数式编程模型,其中可以使用基于 lambda 的调用。您需要通过声明RouterFunctionbean 和返回带有MonoFlux类型的响应的端点处理程序来提供路由端点。

    @Configuration
    public class ToDoRoutingConfiguration {
    
          @Bean
          public RouterFunction<ServerResponse>
                      monoRouterFunction(ToDoHandler toDoHandler) {
                return
        route(GET("/todo/{id}")
    .and(accept(APPLICATION_JSON)),toDoHandler::getToDo)
          .andRoute(GET("/todo")
         .and(accept(APPLICATION_JSON)), toDoHandler::getToDos);
          }
    }
    
    @Component
    public class ToDoHandler {
    
      public Mono<ServerResponse> getToDo(ServerRequest request){
                // ...
          }
    
      public Mono<ServerResponse> getToDos(ServerRequest request){
                // ...
          }
    }
    
    

过滤器异常处理器CORS视图技术web 安全的处理方式与 Spring MVC 相同。这就是使用 Spring Framework 的美妙之处——不管新技术如何,都是一个一致的生态系统。

web 客户端

WebFlux 模块还引入了一个用于 HTTP 请求的反应式非阻塞客户机,它具有函数式 API 客户机和反应式流支持。WebClient界面有以下特点。

  • 一个利用 lambda 编程风格的函数式 API

  • 非阻塞和反应性

  • 用较少的硬件资源支持高并发性

  • 支持从服务器向上和向下流式传输

  • 支持同步和异步通信

WebClient非常容易使用。这个客户端有retrieveexchange方法来获取响应体并解码。

WebClient client = WebClient.create("http://my-to-dos.com");

// [0|1] ToDo
Mono<ToDo> result = client
      .get()

      .uri("/todo/{id}", id)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(ToDo.class);

//[0|N] ToDos
Flux<ToDo> result = client
     .get()
     .uri("/todo").accept(MediaType.TEXT_EVENT_STREAM)
     .retrieve()
     .bodyToFlux(ToDo.class);

稍后,我们将创建一个使用该客户端的小示例,但是如果您有兴趣了解更多相关信息,请查看 https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client

WebFlux 和 Spring Boot 自动配置

有了 Spring Boot,Spring WebFlux 比以往任何时候都容易,因为 Spring Boot 通过为HttpMessageReaderHttpMessageWriter实例配置必要的编解码器来提供自动配置。它支持服务静态资源,包括对 WebJars 的支持。它采用了支持 WebFlux 的最新模板引擎技术,如 FreeMarker ( https://freemarker.apache.org/ )、百里叶( www.thymeleaf.org )、Mustache ( https://mustache.github.io/ )。默认情况下,自动配置将 Netty ( https://netty.io )设置为主容器。

如果您需要覆盖默认的 WebFlux 自动配置,您可以添加自己的类型为WebFluxConfigurer@Configuration类。

重要说明

如果您想完全控制 WebFlux 的自动配置,那么您需要添加您的自定义的用@EnableWebFlux注释的@Configuration

结合 Spring Boot 使用 WebFlux

要将 WebFlux 与 Spring Boot 一起使用,需要将spring-boot-starter-webflux依赖项添加到您的pom.xmlbuild.gradle文件中。

重要说明

您可以同时使用spring-boot-starter-webspring-boot-starter-webflux,但是 Spring Boot 自动配置是 Spring MVC。如果你想使用 WebFlux 的所有功能,那么你需要使用SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)

带有 WebFlux 的 ToDo 应用

让我们从使用 WebFlux 模块创建 ToDo 应用开始。让我们使用新的反应通量和 Mono APIs。

打开你喜欢的浏览器,指向 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-webflux

  • 名称:todo-webflux

  • 包名:com.apress.todo

  • 依赖关系:Lombok, Reactive Web

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以点击生成项目按钮来下载一个 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 6-4 )。

img/340891_2_En_6_Fig4_HTML.jpg

图 6-4

spring initializehttps://start.spring.io

这一次您使用了反应式 Web 依赖项;在这种情况下,spring-boot-starter-webflux启动器。让我们创建ToDo域类,它类似于其他项目(参见清单 6-4 )。

package com.apress.todo.domain;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class ToDo {

    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        this.id = UUID.randomUUID().toString();
        this.created = LocalDateTime.now();
        this.modified = LocalDateTime.now();
    }

    public ToDo(String description){
        this();
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this();
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-4com.apress.todo.domain.ToDo.java

清单 6-4 显示了ToDo类;正如您所看到的,除了默认构造函数中的初始化之外,没有什么新的东西。

接下来,让我们创建ToDoRepository类,它保存内存中的 ToDo(参见清单 6-5 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Arrays;

public class ToDoRepository {

    private Flux<ToDo> toDoFlux =
Flux.fromIterable(Arrays.asList(
            new ToDo("Do homework"),
            new ToDo("Workout in the mornings", true),
            new ToDo("Make dinner tonight"),
            new ToDo("Clean the studio", true)));

    public Mono<ToDo> findById(String id){
       return Mono
            .from(
            toDoFlux.filter( todo -> todo.getId().equals(id)));
    }

    public Flux<ToDo> findAll(){
        return toDoFlux;
    }
}

Listing 6-5com.apress.todo.repository.ToDoRepository.java

清单 6-5 显示了ToDoRepository类,其中toDoFlux实例处理一个Flux<ToDo>。看看从通量返回一个Mono<ToDo>findById方法。

使用带注释的控制器

让我们继续创建一些你已经知道的东西(类似于 Spring MVC):一个处理FluxMono类型的ToDoController类(参见清单 6-6 )。

package com.apress.todo.controller;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class ToDoController {

    private ToDoRepository repository;

    public ToDoController(ToDoRepository repository){
        this.repository = repository;
    }

    @GetMapping("/todo/{id}")
    public Mono<ToDo> getToDo(@PathVariable String id){
        return this.repository.findById(id);
    }

    @GetMapping("/todo")
    public Flux<ToDo> getToDos(){
        return this.repository.findAll();
    }

}

Listing 6-6com.apress.todo.controller.ToDoController.java

从代码中可以看出,你返回的是一个Mono<ToDo>或者一个Flux<ToDo>,和 Spring MVC 不同;请记住,我们正在进行异步和非阻塞调用。

您可以运行应用,进入浏览器并指向http://localhost:8080/todo来查看结果 ToDo 的 JSON 响应;或者,您可以在终端中执行以下命令,并看到相同的输出。

$ curl http://localhost:8080/todo
[
    {

        "completed": false,
        "created": "2018-08-14T20:46:05.542",
        "description": "Do homework",
        "id": "5520e646-47aa-4be6-802a-ef6df500d6fb",
        "modified": "2018-08-14T20:46:05.542"
    },
    {
        "completed": true,
        "created": "2018-08-14T20:46:05.542",
        "description": "Workout in the mornings",
        "id": "3fe07f8d-64b0-4a39-ab1b-658bde4815d7",
        "modified": "2018-08-14T20:46:05.542"
    },
      ...
]

在控制台日志中,请注意运行容器是 Netty 容器,这是 Spring Boot 自动配置的默认容器。

使用功能端点

记住 Spring WebFlux 使用函数式编程来创建反应式 web 应用。让我们创建声明路由函数的ToDoRouter类(参见清单 6-7 )。

package com.apress.todo.reactive;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class ToDoRouter {

    @Bean

    public RouterFunction<ServerResponse> monoRouterFunction(ToDoHandler toDoHandler) {
        return route(GET("/todo/{id}").and(accept(APPLICATION_JSON)), toDoHandler::getToDo)
                .andRoute(GET("/todo").and(accept(APPLICATION_JSON)), toDoHandler::getToDos);
    }
}

Listing 6-7com.apress.todo.reactive.ToDoRouter.java

清单 6-7 显示了使用的路由(端点)和处理程序。Spring WebFlux 提供了一个非常好的流畅的 API 来轻松构建任何路线。

接下来,让我们创建具有服务请求的逻辑的ToDoHandler类(参见清单 6-8 )。

package com.apress.todo.reactive;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

@Component
public class ToDoHandler {

    private ToDoRepository repository;

    public ToDoHandler(ToDoRepository repository){
        this.repository = repository;
    }

    public Mono<ServerResponse> getToDo(ServerRequest request) {
        String toDoId = request.pathVariable("id");
        Mono<ServerResponse> notFound =
ServerResponse.notFound().build();
        Mono<ToDo> toDo = this.repository.findById(toDoId);

        return toDo
                .flatMap(t ->
                          ServerResponse
                        .ok()
                        .contentType(APPLICATION_JSON)
                        .body(fromObject(t)))
                .switchIfEmpty(notFound);
    }

    public Mono<ServerResponse> getToDos(
                                          ServerRequest request) {
        Flux<ToDo> toDos = this.repository.findAll();
        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(toDos, ToDo.class);
    }
}

Listing 6-8com.apress.todo.reactive.ToDoHandler.java

清单 6-8 向您展示了处理程序。我们来分析一下。

  • Mono<ServerResponse>。这种响应类型用在两种方法上,它使用带有BodyBuilder fluent API 的ServerResponse接口,为响应添加一个主体。BodyBuilder接口提供了有用的方法,可以帮助您构建响应,比如用ok方法添加状态。您可以使用headers方法添加标题,等等。

在运行应用之前,从 Spring 容器中移除ToDoController类是很重要的。您可以通过注释掉@RestController注释来做到这一点。

运行应用,您会得到和以前一样的结果。我知道这是一个非常简单的例子,因为这个应用在内存中做所有的事情,对吗?好吧,那就加上反应式持久吧!

反应数据

Spring Data 团队使用 RxJava 和 Project Reactor 实现的动态 API 创建了反应式存储库。这个抽象定义了几种包装器类型。Spring Data 在后台转换反应式包装器类型,这样您就可以坚持使用自己喜欢的组合库,这对于开发人员来说更容易。

  • ReactiveCrudRepository

  • ReactiveSortingRepository

  • RxJava2CrudRepository

  • RxJava2SortingRepository

Spring Data 提供了不同的 reactive 模块:MongoDB、Redis 和 Cassandra Reactive Streams,为您提供了 Reactive Streams 计划的所有灵活性和所有好处。

MongoDB 反应流

Spring Data MongoDB 构建在 MongoDB 反应流之上,它提供了反应流的最大互操作性。它提供了使用FluxMono类型的ReactiveMongoOperations助手接口。

要使用 MongoDB Reactive Streams,有必要将spring-boot-starter-data-mongodb-reactive依赖项包含到您的pom.xmlbuild.gradle文件中。MongoDB Reactive Streams 还为存储库声明提供了一个专用接口,即ReactiveMongoRepository<T,ID>接口。遵循相同的存储库模式,您可以声明自己的名为方法的查询,该查询返回一个FluxMono类型。

带有反应数据的待办事项应用

让我们通过使用 MongoDB 添加反应性数据来完成 ToDo 应用,并使用函数式编程模式对任何请求和响应使用 WebFlux。

可以打开自己喜欢的浏览器,指向 Spring Initializr(https://start.spring.io);将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-reactive-data

  • 名称:todo-reactive-data

  • 包名:com.apress.todo

  • 依赖关系:Lombok, Reactive Web, Reactive MongoDB

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮来下载 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 6-5 )。

img/340891_2_En_6_Fig5_HTML.jpg

图 6-5

spring initializehttps://start.spring.io

如您所见,我们现在有了反应式 Web 和反应式 MongoDB 依赖项。因为我们用的是 MongoDB,所以不需要有服务器。你用的是嵌入式 Mongo 通常,它是用于测试,但我们将在这个应用中使用它。

让我们从添加嵌入式 Mongo 依赖项开始。如果您使用的是 Maven,打开您的pom.xml文件并添加以下依赖项。

<dependency>
     <groupId>de.flapdoodle.embed</groupId>
     <artifactId>de.flapdoodle.embed.mongo</artifactId>
     <scope>runtime</scope>
  </dependency>

如果您使用的是 Gradle,您可以打开build.gradle文件并添加以下依赖项。

runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

接下来,让我们创建ToDo域类(参见清单 6-9 )。

package com.apress.todo.domain;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.LocalDateTime;
import java.util.UUID;

@Document

@Data
public class ToDo {

    @Id
    private String id;
    private String description;
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){
        this.id = UUID.randomUUID().toString();
        this.created = LocalDateTime.now();
        this.modified = LocalDateTime.now();
    }

    public ToDo(String description){
        this();
        this.description = description;
    }

    public ToDo(String description, boolean completed){
        this();
        this.description = description;
        this.completed = completed;
    }
}

Listing 6-9com.apress.todo.domain.ToDo.java

这个类使用了@Document@Id注释,将它们标记为 MongoDB 的持久类。

接下来,让我们创建ToDoRepository接口(参见清单 6-10 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ToDoRepository extends
            ReactiveMongoRepository<ToDo, String> {
}

Listing 6-10com.apress.todo.repository.ToDoRepository.java

这个接口是从ReactiveMongoRepository<T,ID>扩展而来的。该接口提供了您已经知道的相同的Repository功能,但是现在它返回了FluxMono类型。记住这和前面的章节是一样的。在这里,您可以定义返回反应类型的自定义命名查询。

对于这个 ToDo 应用,我们将使用函数式编程。让我们在ToDoRouter类中创建路由器和处理器(参见清单 6-11 )。

package com.apress.todo.reactive;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class ToDoRouter {

    @Bean
    public RouterFunction<ServerResponse>
            monoRouterFunction(ToDoHandler toDoHandler) {
        return
      route(GET("/todo/{id}").and(accept(APPLICATION_JSON)), toDoHandler::getToDo)

.andRoute(GET("/todo").and(accept(APPLICATION_JSON)), toDoHandler::getToDos)

.andRoute(POST("/todo").and(accept(APPLICATION_JSON)), toDoHandler::newToDo);
    }
}

Listing 6-11com.apress.todo.reactive.ToDoRouter.java

清单 6-11 显示了端点声明。有一个新方法,一个帖子。接下来,让我们创建处理程序(参见清单 6-12 )。

package com.apress.todo.reactive;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
import static org.springframework.web.reactive.function.BodyInserters.fromPublisher;

@Component
public class ToDoHandler {

    private ToDoRepository repository;

    public ToDoHandler(ToDoRepository repository){
        this.repository = repository;
    }

    public Mono<ServerResponse> getToDo(ServerRequest request) {
        return findById(request.pathVariable("id"));
    }

    public Mono<ServerResponse> getToDos(ServerRequest request) {
        Flux<ToDo> toDos = this.repository.findAll();
        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(toDos, ToDo.class);
    }

    public Mono<ServerResponse> newToDo(ServerRequest request) {
        Mono<ToDo> toDo = request.bodyToMono(ToDo.class);

        return ServerResponse
                .ok()
                .contentType(APPLICATION_JSON)
                .body(fromPublisher(toDo.flatMap(this::save),ToDo.class));
    }

    private Mono<ServerResponse> findById(String id){
        Mono<ToDo> toDo = this.repository.findById(id);

        Mono<ServerResponse> notFound = ServerResponse.notFound().build();

        return toDo
                .flatMap(t -> ServerResponse
                        .ok()

                        .contentType(APPLICATION_JSON)
                        .body(fromObject(t)))
                .switchIfEmpty(notFound);
    }

    private Mono<ToDo> save(ToDo toDo) {
        return Mono.fromSupplier(
                () -> {
                    repository
                            .save(toDo)
                            .subscribe();
                    return toDo;
                });
    }
}

Listing 6-12com.apress.todo.reactive.ToDoHandler.java

清单 6-12 显示了处理端点响应的处理器。每个方法都返回一个Mono<ServerResponse>。看看使保存到 MongoDB 服务器成为可能的Mono操作符,以及BodyBuilder如何创建响应。

接下来,创建设置到嵌入式 MongoDB 服务器的连接的配置。创建ToDoConfig类(参见清单 6-13 )。

package com.apress.todo.config;

import com.apress.todo.domain.ToDo;
import com.apress.todo.repository.ToDoRepository;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;

@Configuration

@EnableReactiveMongoRepositories(basePackages = "com.apress.todo.repository")

public class ToDoConfig extends AbstractReactiveMongoConfiguration {

    private final Environment environment;

    public ToDoConfig(Environment environment){
        this.environment = environment;
    }

    @Override
    protected String getDatabaseName() {
        return "todos";
    }

    @Override
    @Bean
    @DependsOn("embeddedMongoServer")
    public MongoClient reactiveMongoClient() {
        int port = environment.getProperty("local.mongo.port", Integer.class);
        return MongoClients.create(String.format("mongodb://localhost:%d", port));
    }

    @Bean
    public CommandLineRunner insertAndView(ToDoRepository repository, ApplicationContext context){
        return args -> {

            repository.save(new ToDo("Do homework")).subscribe();
            repository.save(new ToDo("Workout in the mornings", true)).subscribe();
            repository.save(new ToDo("Make dinner tonight")).subscribe();
            repository.save(new ToDo("Clean the studio", true)).subscribe();

            repository.findAll().subscribe(System.out::println);
        };
    }
}

Listing 6-13com.apress.todo.config.ToDoConfig.java

清单 6-13 显示了使用 MongoDB 反应流嵌入式服务器所需的配置。我们来分析一下这个类。

  • @EnableReactiveMongoRepositories。为反应流 API 设置所有必要的基础设施需要这个注释。告诉这个注释存储库在哪里也很重要(如果 repos 是包的一部分,这是不必要的)。

  • AbstractReactiveMongoConfiguration。要设置嵌入式 Mongo,有必要从这个抽象类扩展并实现reactiveMongoClientgetDatabaseName方法。reactiveMongoClient创建MongoClient实例,该实例连接到嵌入式 MongoDB 端口设置的任何地方(由于环境属性local.mongo.port)。

  • @DependsOn。这个注释是在embeddedMongoServer bean 之后创建reactiveMongoClient的助手。

该类还运行代码,其中 ToDo 保存到 MongoDB。

现在,您已经准备好运行 ToDo 应用了。运行应用后,您可以进入浏览器或执行以下命令。

$ curl http://localhost:8080/todo
[
    {
        "completed": false,
        "created": "2018-08-14T20:46:05.542",
        "description": "Do homework",
        "id": "5520e646-47aa-4be6-802a-ef6df500d6fb",
        "modified": "2018-08-14T20:46:05.542"
    },
    {
        "completed": true,
        "created": "2018-08-14T20:46:05.542",
        "description": "Workout in the mornings",
        "id": "3fe07f8d-64b0-4a39-ab1b-658bde4815d7",
        "modified": "2018-08-14T20:46:05.542"
    },
      ...
]

您可以通过执行以下命令来添加新的 ToDo。

$ curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read a book"}' http://localhost:8080/todo
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 164

{
"id":"a3133b8d-1d8b-4b2e-b7d9-48a73999a104",
"description":"Read a book",
"created":"2018-08-14T22:51:19.734",
"modified":"2018-08-14T22:51:19.734",
"completed":false
}

恭喜你!现在,您知道如何使用 Spring WebFlux 和 Project Reactor 的强大功能创建一个反应式、异步和非阻塞的应用了!

注意

所有代码都可以从 Apress 网站上获得。您还可以在 https://github.com/felipeg48/pro-spring-boot-2nd 资源库获取最新信息。

摘要

本章讨论了如何使用 Spring 框架的新成员 WebFlux。您了解了 WebFlux 是一个非阻塞、异步和反应式框架,它实现了反应式流。

您还了解了 WebFlux 提供了两种使用 Spring Boot 编程的方式:带注释的控制器和功能端点,在这里您可以定义 Flux 和 Mono 响应类型。您了解到 WebFlux 还提供了一个WebClient接口来使用这些新的反应式 API。

您了解了 Spring Boot 通过使用spring-boot-starter-webflux为反应式堆栈提供自动配置,默认情况下使用 Netty 作为服务器容器。

下一章将测试这些应用,并展示 Spring Boot 如何让 TDD 的应用变得更加容易。

七、Spring Boot 测试

软件测试是执行程序或系统以发现错误或缺陷(通常称之为 bug)并确保每个程序或系统真正工作的过程。

这是你能在互联网上找到的许多定义之一。

许多公司通过创建令人惊叹的框架,尽一切努力寻找正确和简单的方法来进行测试,这种做法被称为 TDD(测试驱动开发)。

TDD 是一个基于在非常短的开发周期内重复的过程;这里,反馈在过程中起着重要的作用,因为开发人员用最少的代码编写代码来传递一个用例。由于有了反馈,代码可以被重构,直到它被最终用户接受。

Spring 测试框架

Spring 框架的主要思想之一是鼓励开发人员创建简单和松散耦合的类,编程到接口,使软件更加健壮和可扩展。Spring 框架提供了使单元和集成测试变得容易的工具(实际上,如果你真的编程接口,你不需要 Spring 来测试你的系统的功能);换句话说,您的应用应该可以使用带有对象的 JUnit 或 TestNG 测试引擎进行测试(通过使用 new 操作符进行简单的实例化),而不需要 Spring 或任何其他容器。

Spring 框架有几个测试包,帮助为应用创建单元和集成测试。它通过提供几个模拟对象(EnvironmentPropertySourceJNDIServlet)来提供单元测试;反应式:ServerHttpRequestServerHttpResponse测试实用程序)帮助测试你的代码。

Spring 框架最常用的测试特性之一是集成测试。其主要目标是

  • 管理测试执行之间的 Spring IoC 容器缓存

  • 事务管理

  • 测试夹具实例的依赖注入

  • 特定于 Spring 的基类

Spring 框架通过在测试中集成ApplicationContext提供了一种简单的测试方法。Spring 测试模块提供了几种使用ApplicationContext的方法,通过编程和注释:

  • BootstrapWith。配置 Spring TestContext框架如何引导的类级注释。

  • @ContextConfiguration。定义类级别的元数据,以确定如何为集成测试加载和配置一个ApplicationContext。这是您的类的必备注释,因为那是ApplicationContext加载所有 bean 定义的地方。

  • @WebAppConfiguration。一个类级注释,声明集成测试的ApplicationContext负载应该是一个WebApplicationContext

  • @ActiveProfile。一个类级注释,用于声明在加载集成测试的ApplicationContext时,哪些 bean 定义概要文件应该是活动的。

  • @TestPropertySource。一个类级注释,用于配置属性文件和内联属性的位置,这些属性将被添加到为集成测试加载的ApplicationContextEnvironment中的PropertySources集合中。

  • @DirtiesContext。表示底层 Spring ApplicationContext在测试执行过程中已经被污染(例如,通过改变单例 bean 的状态而被修改或破坏),应该被关闭。

还有更多的(@TestExecutionListeners@Commit@Rollback@BeforeTransaction@AfterTransaction@Sql@SqlGroup@SqlConfig@Timed@Repeat@IfProfileValue等等。

如您所见,当您使用 Spring 框架进行测试时,有很多选择。通常,您总是使用@RunWith注释来连接所有的测试框架。举个例子,

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class ToDoRepositoryTests {

      @Test
      public void ToDoPersistenceTest(){
            //...
      }
}

现在,让我们看看如何使用 Spring 测试框架以及 Spring Boot 提供的所有特性。

Spring Boot 测试框架

Spring Boot 通过增强和添加新的注释和特性来使用 Spring 测试框架的强大功能,使得开发人员的测试更加容易。

如果你想开始使用 Spring Boot 的所有测试特性,你只需要在你的应用中添加对scope testspring-boot-starter-test依赖。这种依赖性已经存在于 Spring Initializr 服务中。

spring-boot-starter-test依赖项提供了几个测试框架,可以很好地配合所有 Spring Boot 测试特性:JUnit、AssertJ、Hamcrest、Mockito、JSONassert 和 JsonPath。当然,还有其他测试框架可以很好地使用 Spring Boot 测试模块;您只需要手动包含这些依赖项。

Spring Boot 提供了@SpringBootTest注解,简化了测试 Spring 应用的方式。通常,在 Spring 测试中,你需要添加几个注释来测试你的应用的特定特性或功能,但在 Spring Boot 中不是这样——尽管你仍然需要使用@RunWith(SpringRunner.class)注释来做测试;否则,任何新的 Spring Boot 测试注释都将被忽略。在测试 web 应用时,@SpringBootTest有一些有用的参数,比如定义一个RANDOM_PORTDEFINED_PORT

下面的代码片段是 Spring Boot 测试的框架。

@RunWith(SpringRunner.class)

@SpringBootTest

public class MyTests {

      @Test
      public void exampleTest() {
            ...
      }
}

测试 Web 端点

Spring Boot 提供了一种测试端点的方法:一个叫做MockMvc类的模拟环境。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcToDoTests {

      @Autowired
      private MockMvc mvc;

      @Test
      public void toDoTest() throws Exception {
          this.mvc
          .perform(get("/todo"))
          .andExpect(status().isOk())
          .andExpect(content()
             .contentType(MediaType.APPLICATION_JSON_UTF8));
      }

}

也可以使用TestRestTemplate类。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ToDoSimpleRestTemplateTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void toDoTest() {
        String body = this.restTemplate.getForObject("/todo", String.class);
        assertThat(body).contains("Read a Book");
    }
}

这段代码展示了一个测试,它运行一个完整的服务器,并使用TestRestTemplate实例调用/todo端点。在这里,我们假设一个String的回报。(这不是测试 JSON 返回的最佳方式。不要担心,稍后您会看到使用TestRestTemplate类的正确方法。

嘲讽豆

Spring Boot 测试模块提供了一个@MockBean注释,为ApplicationContext中的 bean 定义了一个 Mockito mock。换句话说,您可以通过添加这个注释来模仿一个新的 Spring bean 或者替换一个现有的定义。记住,这发生在ApplicationContext内部。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ToDoSimpleMockBeanTests {

    @MockBean
    private ToDoRepository repository;

    @Test
    public void toDoTest() {
        given(this.repository.findById("my-id"))
            .Return(new ToDo("Read a Book"));
        assertThat(
            this.repository.findById("my-id").getDescription())
            .isEqualTo("Read a Book");
    }
}

Spring Boot 测试切片

Spring Boot 提供的最重要的特性之一是不需要特定的基础设施就可以执行测试。Spring Boot 测试模块包括来测试应用的特定部分,而不需要服务器或数据库引擎。

@JsonTest

Spring Boot 测试模块提供了@JsonTest注释,这有助于对象 JSON 序列化和反序列化,并验证一切都按预期工作。@JsonTest根据类路径中的库:Jackson、GSON 或 JSONB,自动配置支持的 JSON 映射器。

@RunWith(SpringRunner.class)

@JsonTest

public class ToDoJsonTests {

    @Autowired
    private JacksonTester<ToDo> json;

    @Test
    public void toDoSerializeTest() throws Exception {
        ToDo toDo = new ToDo("Read a Book");

        assertThat(this.json.write(toDo))
        .isEqualToJson("todo.json");

        assertThat(this.json.write(toDo))
        .hasJsonPathStringValue("@.description");

        assertThat(this.json.write(toDo))
        .extractingJsonPathStringValue("@.description")
        .isEqualTo("Read a Book");
    }

    @Test
    public void toDoDeserializeTest() throws Exception {
        String content = "{\"description\":\"Read a Book\",\"completed\": true }";
        assertThat(this.json.parse(content))
                .isEqualTo(new ToDo("Read a Book", true));
         assertThat(
           this.json.parseObject(content).getDescription())
         .isEqualTo("Read a Book");
    }
}

@WebMvcTest

如果您需要在不使用完整服务器的情况下测试您的控制器,Spring Boot 提供了@WebMvcTest注释,该注释自动配置 Spring MVC 基础设施并将扫描的 beans 限制为@Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterWebMvcConfigurerHandlerMethodArgumentResolver;这样你就知道你的控制器是否按预期工作。

重要的是要知道,当使用这个注释时,标记为@Component的 beans 不会被扫描,但是如果需要的话,您仍然可以使用@MockBean

@RunWith(SpringRunner.class)

@WebMvcTest(ToDoController.class)

public class ToDoWebMvcTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private ToDoRepository toDoRepository;

    @Test
    public void toDoControllerTest() throws Exception {
        given(this.toDoRepository.findById("my-id"))
                .Return(new ToDo("Do Homework", true));

        this.mvc.perform(get("/todo/my-id").accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk()).andExpect(content().string("{\"id\":\"my-id\",\"description\":\"Do Homework\",\"completed\":true}"));
    }

}

@WebFluxTest

Spring Boot 为任何反应式控制器提供了@WebFluxTest注释。这个注释自动配置 Spring WebFlux 模块基础设施,并且只扫描@Controller@ControllerAdvice@JsonComponentConverterGenericConverterWebFluxConfigurer

重要的是要知道,当使用这个注释时,标记为@Component的 beans 不会被扫描,但是如果需要的话,您仍然可以使用@MockBean

@RunWith(SpringRunner.class)

@WebFluxTest(ToDoFluxController.class)

public class ToDoWebFluxTest {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private ToDoRepository toDoRepository;

    @Test
    public void testExample() throws Exception {
        given(this.toDoRepository.findAll())
                .Return(Arrays.asList(new ToDo("Read a Book"), new ToDo("Buy Milk")));
        this.webClient.get().uri("/todo-flux").accept(MediaType.APPLICATION_JSON_UTF8)
                .exchange()
                .expectStatus().isOk()
                .expectBody(List.class);
    }
}

@DataJpaTest

如果您需要测试您的 JPA 应用,Spring Boot 测试模块提供了@DataJpaTest,它自动配置内存中的嵌入式数据库。它扫描@Entity。这个注释不会加载任何@Component bean。它还提供了与 JPA EntityManager类非常相似的TestEntityManager助手类,专门用于测试。

@RunWith(SpringRunner.class)

@DataJpaTest

public class TodoDataJpaTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private ToDoRepository repository;

    @Test
    public void toDoDataTest() throws Exception {
        this.entityManager.persist(new ToDo("Read a Book"));
        Iterable<ToDo> toDos = this.repository.findByDescriptionContains("Read a Book");
        assertThat(toDos.iterator().next()).toString().contains("Read a Book");
    }

}

请记住,使用@DataJpaTest使用了嵌入式内存数据库引擎(H2、Derby、HSQL),但是如果您想使用真实的数据库进行测试,您需要添加下面的@AutoConfigureTestDatabase(replace=Replace.NONE)注释作为测试类的标记。

@RunWith(SpringRunner.class)

@DataJpaTest

@AutoConfigureTestDatabase(replace=Replace.NONE)

public class TodoDataJpaTests {
      //...
}

@JdbcTest

这个注释和@DataJpaTest很像;唯一的区别是,它只做与 JDBC 相关的测试。它自动配置内存中的嵌入式数据库引擎,并配置JdbcTemplate类。它省略了所有标记为@Component的类。

@RunWith(SpringRunner.class)

@JdbcTest

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class TodoJdbcTests {

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    private CommonRepository<ToDo> repository;

      @Test
      public void toDoJdbcTest() {
          ToDo toDo = new ToDo("Read a Book");

        this.repository = new ToDoRepository(jdbcTemplate);
        this.repository.save(toDo);

        ToDo result = this.repository.findById(toDo.getId());
        assertThat(result.getId()).isEqualTo(toDo.getId());
      }

}

@DataMongoTest

Spring Boot 测试模块提供了@DataMongoTest注释来测试 Mongo 应用。它自动配置内存中的嵌入式 Mongo 服务器(如果可用),如果没有,您需要添加正确的spring.data.mongodb.*属性。它配置MongoTemplate类并扫描@Document注释。@Component豆子不会被扫描。

@RunWith(SpringRunner.class)

@DataMongoTest

public class TodoMongoTests {

    @Autowired
    private MongoTemplate mongoTemplate;

      @Test
      public void toDoMongoTest() {
        ToDo toDo = new ToDo("Read a Book");
        this.mongoTemplate.save(toDo);

        ToDo result = this.mongoTemplate.findById(toDo.getId(),ToDo.class);
        assertThat(result.getId()).isEqualTo(toDo.getId());
      }

}

如果您需要一个外部 MongoDB 服务器(不是内嵌在内存中的),请将excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class参数添加到@DataMongoTest注释中。

@RunWith(SpringRunner.class)

@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)

public class ToDoMongoTests {
      // ...
}

@RestClientTest

另一个重要的注释是@RestClientTest,它测试您的 REST 客户端。这个注释自动配置 Jackson、GSON 和 JSONB 支持。它配置了RestTemplateBuilder并增加了对MockRestServiceServer的支持。

@RunWith(SpringRunner.class)

@RestClientTest(ToDoService.class)

public class ToDoRestClientTests {

    @Autowired
    private ToDoService service;

    @Autowired
    private MockRestServiceServer server;

    @Test
    public void toDoRestClientTest()
            throws Exception {
        String content = "{\"description\":\"Read a Book\",\"completed\": true }";
        this.server.expect(requestTo("/todo/my-id"))
                .andRespond(withSuccess(content,MediaType.APPLICATION_JSON_UTF8));
        ToDo result = this.service.findById("my-id");
        assertThat(result).isNotNull();
        assertThat(result.getDescription()).contains("Read a Book");
    }

}

还有很多其他的切片可以看看。需要注意的重要一点是,您不需要有一个完整的基础设施或者运行服务器来进行测试。切片有助于 Spring Boot 应用的更多测试。

摘要

在本章中,您学习了用 Spring Boot 测试应用的不同方法。尽管这一章很短,我还是向您展示了一些重要的特性,比如切片。

在下一章中,我们将讨论安全性,并了解 Spring Boot 如何保护我们的应用。

八、Spring Boot 的安全

本章向您展示了如何在您的 Spring Boot 应用中使用安全性来保护您的 web 应用。从使用基本安全性到使用 OAuth,您可以学到很多东西。在过去的十年中,安全性已经成为桌面、web 和移动应用的首要和重要因素。但是安全性有点难以实现,因为您需要考虑所有的事情——跨站点脚本、授权和认证、安全会话、身份识别、加密等等。在应用中实现简单的安全性还有很多工作要做。

Spring security 团队努力让开发人员更容易为他们的应用带来安全性,从保护服务方法到整个 web 应用。春安围绕AuthenticationProviderAuthenticationManager,专业化UserDetailsService;它还提供了与身份提供者系统的集成,如 LDAP、Active Directory、Kerberos、PAM、OAuth 等。你将在本章的例子中回顾其中的一些。

Spring Security

Spring Security 是高度可定制的强大框架,有助于身份验证和授权(或访问控制);它是保护 Spring 应用的默认模块。以下是一些重要的功能。

  • Servlet API 集成

  • 与 Spring Web MVC 和 WebFlux 的集成

  • 防范诸如会话固定、点击劫持、CSRF(跨站点请求伪造)、CORS(跨源资源共享)等攻击

  • 对认证和授权的可扩展和全面的支持

  • 与这些技术的集成:HTTP Basic、HTTP Digest、X.509、LDAP、基于表单、OpenID、CAS、RMI、Kerberos、JAAS、Java EE 等等

  • 与第三方技术的集成:AppFuse、DWR、Grails、Tapestry、JOSSO、AndroMDA、Roller 等等

Spring Security 已经成为在许多 Java 和 Spring 项目上使用安全性的事实上的方法,因为它以最小的努力集成和定制,创建健壮和安全的应用。

与 Spring Boot 的安全

Spring Boot 使用 Spring Security 框架的力量来保护应用。要使用 Spring Security 性,有必要添加spring-boot-starter-security依赖项。这个依赖关系提供了所有的spring-security核心 jar,它自动配置策略来决定是使用httpBasic还是formLogin认证机制。它默认为单用户的UserDetailService。这个用户名是user,当应用启动时,密码被打印(随机字符串)为一个带有 INFO 级别的日志。

换句话说,通过添加spring-boot-starter-security依赖项,您的应用已经安全了。

具有基本安全性的 ToDo 应用

先说 ToDo app。这里,您使用与 JPA REST 项目相同的代码;但是我会再复习一遍。让我们开始吧。从头开始,打开你的浏览器,打开 Spring Initializr ( https://start.spring.io )。将以下值添加到字段中:

  • 组:com.apress.todo

  • 神器:todo-simple-security

  • 名称:todo-simple-security

  • 包名:com.apress.todo

  • 依赖关系:Web, Security, Lombok, JPA, REST Repositories, H2, MySQL, Mustache

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以按下生成项目按钮;这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 8-1 )。

img/340891_2_En_8_Fig1_HTML.jpg

图 8-1

Spring 初始化 zr

这个项目现在有了安全模块和模板引擎 Mustache。很快你就会知道如何使用它。

让我们从 ToDo 域类开始(参见清单 8-1 )。

package com.apress.todo.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-1com.apress.todo.domain.ToDo.java

清单 8-1 显示了ToDo域类。你已经知道了。它标有@Entity,并且使用@Id作为主键。这个类来自于 todo-rest 项目。

接下来,让我们回顾一下ToDoRepository接口(见清单 8-2 )。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> {

}

Listing 8-2com.apress.todo.repository.ToDoRepository.java

清单 8-2 显示的是ToDoRepository,当然,你已经知道了。定义从CrudRepository<T,ID>扩展的接口,该接口不仅有 CRUD 方法,还有 Spring Data REST,创建所有必要的 REST APIs 来支持域类。

接下来,让我们回顾一下application.properties,看看有什么新内容(参见清单 8-3 )。

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2-Console: http://localhost:8080/h2-console
# jdbc:h2:mem:testdb
spring.h2.console.enabled=true

# REST API

spring.data.rest.base-path=/api

Listing 8-3src/main/resources/application.properties

清单 8-3 显示了application.properties文件。除了最后一个,你已经看过一些房产了,对吗?spring.data.rest.base-path告诉 REST controller(Spring Data REST 配置的)使用/api作为根来公开所有的 REST API 端点。因此,如果我们想获得 ToDo,我们需要在http://localhost:8080/api/toDos访问端点。

在运行应用之前,让我们以脚本的形式添加端点。用下面的 SQL 语句创建src/main/resources/data.sql文件。

insert into to_do (id,description,created,modified,completed)
values ('8a8080a365481fb00165481fbca90000', 'Read a Book','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137',true);

insert into to_do (id,description,created,modified,completed)
values ('ebcf1850563c4de3b56813a52a95e930', 'Buy Movie Tickets','2018-08-17 09:50:10.126','2018-08-17 09:50:10.126',false);

insert into to_do (id,description,created,modified,completed)
values ('78269087206d472c894f3075031d8d6b', 'Clean my Room','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137',false);

现在,如果您运行您的应用,您应该在日志中看到以下输出:

Using generated security password: 2a569843-122a-4559-a245-60f5ab2b6c51

这是你的密码。您现在可以进入浏览器并打开https://localhost:8080/api/toDos。当你按下回车键访问那个 URL 时,你会得到类似于图 8-2 的东西。

img/340891_2_En_8_Fig2_HTML.jpg

图 8-2

所有应用:http://localhost:8080/log in 页

图 8-2 显示了一个登录页面,这是添加spring-boot-starter-security依赖项时的默认行为。默认情况下,安全是开启的—如此简单!!那么,用户和密码是什么呢?嗯,我之前提到过,用户是user,密码是随机打印在日志中的(在本例中是2a569843-122a-4559-a245-60f5ab2b6c51)。所以,继续输入用户名和密码;然后你应该得到待办事项列表(见图 8-3 )。

img/340891_2_En_8_Fig3_HTML.jpg

图 8-3

http://localhost:80808/API/全部

如果您想尝试使用命令行,您可以在终端窗口中执行以下命令。

$ curl localhost:8080/api/toDos
{"timestamp":"2018-08-19T21:25:47.224+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/api/toDos"}

正如您在输出中看到的,您无权进入该端点。需要认证吧?您可以执行以下命令。

$ curl localhost:8080/api/toDos -u user:2a569843-122a-4559-a245-60f5ab2b6c51
{
  "_embedded" : {
    "toDos" :  {
      "description" : "Read a Book",
      "created" : "2018-08-17T07:42:44.136",
      "modified" : "2018-08-17T07:42:44.137",
      "completed" : true,
...
}

正如您现在看到的,您正在传递用户名和随机密码,并且您正在得到带有待办事项列表的响应。

可能你已经知道,每次你重启这个应用,安全自动配置生成另一个随机密码,这不是最佳的;也许只是为了发展。

覆盖简单安全性

随机密码在生产环境中不起作用。Spring Boot 安全性允许您以多种方式覆盖默认值。最简单的方法是通过添加下面的spring.security.*属性,用application.properties文件覆盖它。

spring.security.user.name=apress
spring.security.user.password=springboot2
spring.security.user.roles=ADMIN,USER

如果再次运行该应用,用户名为apress,密码为springboot2(与命令行中相同)。还要注意,在日志中,随机密码不再打印。

另一种方法是以编程方式提供身份验证。创建一个从WebSecurityConfigureAdapter扩展而来的ToDoSecurityConfig类。看看清单 [8-4 。

package com.apress.todo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(
      AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                      .passwordEncoder(passwordEncoder())
                      .withUser("apress")
                      .password(passwordEncoder().encode("springboot2"))
                      .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Listing 8-4com.apress.todo.config.ToDoSecurityConfig.java

清单 8-4 显示了以编程方式构建安全性的必要配置,在这种情况下,只有一个用户(当然,您可以添加更多用户)。我们来分析一下代码。

  • WebSecurityConfigurerAdapter。扩展该类是重写安全性的一种方式,因为它允许您重写您真正需要的方法。在这种情况下,代码覆盖了configure(AuthenticationManagerBuilder)签名。

  • AuthenticationManagerBuilder。这个类创建了一个AuthenticationManager,允许您轻松构建内存、LDAP、JDBC 认证、UserDetailsService并添加AutheticationProvider。在本例中,您正在构建一个内存认证。有必要添加一个PasswordEncoder和一个新的更安全的方法来使用和加密/解密密码。

  • BCryptPasswordEncoder。在这段代码中,您使用了使用 BCrypt 强散列函数的BCryptPasswordEncoder(返回一个PasswordEncoder实现)。您也可以使用Pbkdf2PasswordEncoder(使用 PBKDF2,具有可配置的迭代次数和一个随机的 8 字节随机 salt 值),或者SCryptPasswordEncoder(使用 SCrypt 散列函数)。更好的是使用DelegatingPasswordEncoder,支持密码升级。

在运行应用之前,注释掉添加到application.properties文件中的spring.security.*属性。如果你运行这个应用,它应该像预期的那样工作。您需要提供用户名apress和密码springboot2

覆盖默认登录页面

Spring Security 允许您以几种方式覆盖默认登录页面。一种方法是配置HttpSecurityHttpSecurity类允许您为特定的 HTTP 请求配置基于 web 的安全性。默认情况下,它适用于所有请求,但是可以使用requestMatcher(RequestMatcher)或类似的方法进行限制。

让我们来看看对ToDoSecurityConfig类的修改(参见清单 8-5 )。

package com.apress.todo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
                .withUser("apress")
                .password(passwordEncoder().encode("springboot2"))
                .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().fullyAuthenticated()
                .and()
                .httpBasic();

    }
}

Listing 8-5com.apress.todo.config.ToDoSecurityConfig.java – v2

清单 8-5 显示了ToDoSecurityConfig类的版本 2。如果你运行应用并进入浏览器(http://localhost:8080/api/toDos),你现在会得到一个基本认证的弹出窗口(见图 8-4 )。

img/340891_2_En_8_Fig4_HTML.jpg

图 8-4

*Http://localhost:8080/API/toDos*—Http 基本认证

您可以使用您已经知道的用户名和密码,并且您应该得到 ToDo 列表。命令行也是如此。你需要认证

$ curl localhost:8080/api/toDos -u apress:springboot2

自定义登录页面

通常在应用中,你不会看到这样的页面;通常情况下,有一个非常漂亮和设计良好的登录页面,对不对?Spring Security 允许您创建和定制您的登录页面。

让我们准备带有登录页面的 ToDo 应用。首先,我们将添加一些 CSS 和众所周知的 jQuery 库。如今,在 Spring Boot 的应用中,我们可以使用 WebJars 依赖。这种新方式避免了手动下载文件;相反,你可以把它们作为资源。Spring Boot web 自动配置为他们创建了必要的访问权限。

如果您使用的是 Maven,打开pom.xml并添加以下依赖项。

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.3.7</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.2.1</version>
</dependency>

如果您正在使用 Gradle,打开您的build.gradle文件并添加以下依赖项。

compile ('org.webjars:bootstrap:3.3.7')
compile ('org.webjars:jquery:3.2.1')

接下来,让我们创建登录页面,它具有.mustache扩展名(login.mustache)。必须在src/main/resources/templates文件夹中创建它(参见清单 8-6 )。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDo's API Login Page</title>
    <link href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="css/signin.css" rel="stylesheet">
  </head>

  <body>

    <div class="container">
      <form class="form-signin" action="/login" method="POST">
        <h2 class="form-signin-heading">Please sign in</h2>

        <label for="username" class="sr-only">Username</label>
        <input type="text"     name="username" class="form-control" placeholder="Username" required autofocus>

        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" name="password" class="form-control" placeholder="Password" required>

        <button class="btn btn-lg btn-primary btn-block" id="login" type="submit">Sign in</button>
        <input type="hidden" name="_csrf" value="{{_csrf.token}}" />
      </form>
    </div>
  </body>
</html>

Listing 8-6src/main/resources/templates/login.mustache

清单 8-6 显示了 HTML 登录页面。本页面使用 CSS 从 Bootstrap ( https://getbootstrap.com )通过 WebJars ( www.webjars.org )依赖。这些文件作为文件资源从这些 jar 中获取。HTML-FORM 正在使用用户名密码作为名称(这是 Spring Security 必须的)。我们需要包含 CSRF 令牌以避免任何攻击。小胡子引擎为此提供了{{_csrf.token}}值。Spring Security 使用同步器令牌模式来避免请求中的任何攻击。稍后,我们将看到如何获得这个值。

接下来,让我们创建一个索引页面,让您可以看到主页并注销。在src/main/resources/templates文件夹中创建index.mustache页面(参见清单 8-7 )。

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDo's API</title>
    <link href="webjars/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="webjars/jquery/3.2.1/jquery.min.js"></script>

</head>

<body>
<div class="container">
    <div class="header clearfix">
        <nav>
            <a href="#" id="logoutLink">Logout</a>
        </nav>
    </div>

    <div class="jumbotron">
        <h1>ToDo's Rest API</h1>
        <p class="lead">Welcome to the ToDo App. A Spring Boot application!</p>
    </div>
</div>

<form id="logout" action="/logout" method="POST">

    <input type="hidden" name="_csrf" value="{{_csrf.token}}" />

</form>

<script>
    $(function(){
        $('#logoutLink').click(function(){
            $('#logout').submit();
        });
    });
</script>
</body>
</html>

Listing 8-7src/main/resources/templates/index.mustache

清单 8-7 显示了索引页面。我们仍然使用 Bootstrap 和 jQuery 资源,以及最重要的部分{{_csrf.token}},用于注销。

接下来,我们先从配置说起。首先,需要修改ToDoSecurityConfig类(参见清单 8-8 )。

package com.apress.todo.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import 

org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(passwordEncoder())
                .withUser("apress")
                .password(passwordEncoder().encode("springboot2"))
                .roles("ADMIN","USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers(
                        PathRequest
                              .toStaticResources()
                              .atCommonLocations()).permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                    .logoutRequestMatcher(
                        new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/login");
    }

}

Listing 8-8com.apress.todo.config.ToDoSecurityConfig.java – v3

清单 8-8 显示了ToDoSecurityConfig类的版本 3。新的修改显示了如何配置HttpSecurity。首先,它添加了指向公共位置的requestMatchers,比如静态资源(static/*)。这是 CSS、JS 或任何其他简单 HTML 可以存在的地方,并且不需要任何安全性。那么它用的是anyRequest,应该是fullyAuthenticated。这意味着/api/*将会。然后,它使用formLoginloginPage("/login")指定它是查找登录页面的端点。接下来,声明注销及其端点("/logout");如果注销成功,它将重定向到"/login"端点/页面。

现在需要告诉 Spring MVC 如何定位登录页面。创建ToDoWebConfig类(参见清单 8-9 )。

package com.apress.todo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ToDoWebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}

Listing 8-9com.apress.todo.config.ToDoWebConfig.java

清单 8-9 展示了在 Spring MVC 中配置 web 控制器的不同方式。您仍然可以使用用@Controller注释的类,并为登录页面创建映射;但这是 JavaConfig 的方式。

这里,该类实现了WebMvcConfigure接口。它实现了addViewControllers方法,并通过告诉控制器视图的位置来注册/login端点。这将定位到templates/login.mustache页面。

最后,有必要通过添加以下属性来更新application.properties文件。

spring.mustache.expose-request-attributes=true

还记得{{_csrf.token}}吗?这就是它获取值的方式——通过添加spring.mustache.expose-request-attributes属性。

现在,您可以运行应用了。如果你去http://localhost:8080,你会得到类似于图 8-5 的东西。

img/340891_2_En_8_Fig5_HTML.jpg

图 8-5

http://localhost:8080/log in

您将获得自定义登录页面。完美!!现在你可以输入凭证,它返回索引页面(见图 8-6 )。

img/340891_2_En_8_Fig6_HTML.jpg

图 8-6

登录后 http://localhost:8080

一旦你有了主页,你就可以访问http://localhost:8080/api/toDos。您应该完全通过了身份验证,并且可以返回到待办事项列表。您可以返回到主页并按下 Logout 链接,它会再次将您重定向到/login端点。

现在,如果您尝试在终端窗口中执行以下命令行,会发生什么情况?

$ curl localhost:8080/api/toDos -u apress:springboot2

它不会返回任何东西。这是一个空行。如果您使用-i标志,它会告诉您正在被重定向到http://localhost:8080/login。但是没有办法从命令行交互,对吧?那么我们能做些什么来解决这个问题呢?事实上,有些客户从来不使用 web 界面。大多数客户端都是应用,并且在编程上需要使用 REST API,但是使用这个解决方案,没有办法进行身份验证来与表单交互。

打开ToDoSecurityConfig类并修改configure(HttpSecurity)方法。它应该类似于下面的代码片段。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .anyRequest().fullyAuthenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                    .logoutRequestMatcher(
                          new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/login")
                .and()
                            .httpBasic();
    }

该方法的最后两行添加了httpBasic调用,它允许客户端(比如 cURL)使用基本的认证机制。您可以重新运行 ToDo 应用,并查看现在执行命令行的工作。

对 JDBC 使用安全性

想象一下,您的公司已经有了一个员工数据库,您希望重用它来对 ToDo 应用进行身份验证和授权。集成这样的东西很好,对吗?

Spring Security 允许您将 AuthenticationManager 与内存、LDAP 和 JDBC 机制结合使用。在本节中,我们将修改 ToDo 应用,使其与 JDBC 一起运行。

JDBC 安全的目录应用

在本节中,您将创建一个新的应用——一个包含所有人员的目录应用。目录应用与 ToDo 应用集成在一起,以进行身份验证和授权。因此,如果客户机需要添加一个新的 ToDo,它需要用一个USER角色进行认证。

让我们开始吧。从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.directory

  • 神器:directory

  • 名称:directory

  • 包名:com.apress.directory

  • 依赖关系:WebSecurityLombokJPAREST RepositoriesH2MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 8-7 )。

img/340891_2_En_8_Fig7_HTML.jpg

图 8-7

Spring 初始化 zr

如您所见,依赖项与其他项目非常相似。我们将利用 Spring Data、安全性和 REST 的力量。让我们从添加一个保存个人信息的新类开始。创建Person类(参见清单 8-10 )。

package com.apress.directory.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Data
@Entity
public class Person {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @Column(unique = true)
    private String email;
    private String name;
    private String password;
    private String role = "USER";
    private boolean enabled = true;
    private LocalDate birthday;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;

    public Person() {
    }

    public Person(String email, String name, String password, String birthday) {
        this.email = email;
        this.name = name;
        this.password = password;
        this.birthday = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyy-MM-dd"));

    }

    public Person(String email, String name, String password, LocalDate birthday) {
        this.email = email;
        this.name = name;
        this.password = password;
        this.birthday = birthday;
    }

    public Person(String email, String name, String password, String birthday, String role, boolean enabled) {
        this(email, name, password, birthday);
        this.role = role;
        this.enabled = enabled;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-10com.apress.directory.domain.Person.java

清单 8-10 显示的是Person类;很简单。它保存了足够的关于一个人的信息。接下来,让我们创建存储库——PersonRepository接口(参见清单 8-11 )。

package com.apress.directory.repository;

import com.apress.directory.domain.Person;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface PersonRepository extends CrudRepository<Person,String> {
    public Person findByEmailIgnoreCase(@Param("email") String email);
}

Listing 8-11com.apress.directory.repository.PersonRepository.java

清单 8-11 显示PersonRepository界面;但是和其他的有什么不同呢?它声明了一个以电子邮件为参数的查询方法 findByEmailIgnoreCase(由@Param注释)。该语法告诉 Spring Data REST 它需要实现这些方法并相应地创建 SQL 语句(这是基于名称和域类中的字段,在本例中是email字段)。

注意

如果您想了解更多关于如何定义自己的查询方法的信息,请查看位于 https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods 的 Spring Data JPA 参考。

接下来,创建从WebSecurityConfigurerAdapter类扩展而来的DirectorySecurityConfig类。记住,通过扩展这个类,我们可以定制为这个应用设置 Spring Security 性的方式(参见清单 8-12 )。

package com.apress.directory.config;

import com.apress.directory.repository.PersonRepository;
import com.apress.directory.security.DirectoryUserDetailsService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class DirectorySecurityConfig extends WebSecurityConfigurerAdapter {

    private PersonRepository personRepository;

    public DirectorySecurityConfig(PersonRepository personRepository){
        this.personRepository = personRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").hasRole("ADMIN")
                .and()
                .httpBasic();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(
            new DirectoryUserDetailsService(this.personRepository));
    }

}

Listing 8-12com.apress.directory.config.DirectorySecurityConfig.java

清单 8-12 显示了DirectorySecurityConfig类。该类通过只允许具有ADMIN角色的用户使用基本身份验证访问任何端点(/**)来配置HttpSecurity

与其他安全配置还有什么不同?你是对的!AuthenticationManager正在配置一个UserDetailsService实现。这是使用任何其他第三方安全应用并将它们与 Spring Security 集成的关键。

如您所见,userDetailsService方法使用了DirectoryUserDetailsService类。让我们创建它(见清单 8-13 )。

package com.apress.directory.security;

import com.apress.directory.domain.Person;
import com.apress.directory.repository.PersonRepository;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

public class DirectoryUserDetailsService implements UserDetailsService {

    private PersonRepository repo;

    public DirectoryUserDetailsService(PersonRepository repo) {
        this.repo = repo;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            final Person person = this.repo.findByEmailIgnoreCase(username);

            if (person != null) {
                PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
                String password = encoder.encode(person.getPassword());

                return User.withUsername(person.getEmail()).accountLocked(!person.isEnabled()).password(password).roles(person.getRole()).build();
            }
        }catch(Exception ex){
            ex.printStackTrace();
        }

        throw new UsernameNotFoundException(username);
    }
}

Listing 8-13com.apress.directory.security.DirectoryUserDetailsService.java

清单 8-13 显示了DirectoryUserDetailsService类。这个类实现了UserDetailsService接口,需要实现loadUserByUserName并返回一个UserDetails实例。在这个实现中,代码显示了如何使用PersonRepository。在这种情况下,它使用findByEmailIgnoreCase;因此,如果在用户想要访问/**(任何端点)时发现一个人有提供的电子邮件,它会通过创建一个UserDetails实例,将电子邮件与提供的密码、角色以及帐户是否锁定进行比较。

太神奇了!这个应用使用 JDBC 作为认证机制。同样,您可以插入任何其他可以实现UserDetailService的安全系统/应用,并返回一个UserDetails实例;就这样。

接下来,让我们快速查看一下application.properties文件,看看它的属性。

# Server
server.port=${port:8181}

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2
spring.h2.console.enabled=true

唯一不同的是它有server.port属性,上面写着:如果你提供变量端口(要么命令行,要么环境)我就用它;如果没有,我就用端口 8181 。就是这个:。这是 SpEL (Spring 表达式语言)的一部分。

在运行目录应用之前,让我们添加一些数据。在src/main/resources文件夹中创建data.sql文件。

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('dc952d19ccfc4164b5eb0338d14a6619','Mark','mark@example.com','secret','USER',true,'1960-03-29','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('02288a3b194e49ceb1803f27be5df457','Matt','matt@example.com','secret','USER',true,'1980-07-03','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('4fe22e358d0e4e38b680eab91787f041','Mike','mike@example.com','secret','ADMIN',true,'19820-08-05','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('84e6c4776dcc42369510c2692f129644','Dan','dan@example.com','secret','ADMIN',false,'1976-10-11','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

insert into person (id,name,email,password,role,enabled,birthday,created,modified)
values ('03a0c396acee4f6cb52e3964c0274495','Administrator','admin@example.com','admin','ADMIN',true,'1978-12-22','2018-08-17 07:42:44.136','2018-08-17 07:42:44.137');

现在,我们准备将这个应用用作身份验证和授权机制。运行目录应用。此应用从 8181 端口启动。您可以使用浏览器和/或cURL命令对其进行测试。

$ curl localhost:8181/persons/search/findByEmailIgnoreCase?email=mark@example.com  -u admin@example.com:admin
{
  "email" : "mark@example.com",
  "name" : "Mark",
  "password" : "secret",
  "role" : "USER",
  "enabled" : true,
  "birthday" : "1960-03-29",
  "created" : "2018-08-17T07:42:44.136",
  "modified" : "2018-08-17T07:42:44.137",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8181/persons/dc952d19ccfc4164b5eb0338d14a6619"
    },
    "person" : {
      "href" : "http://localhost:8181/persons/dc952d19ccfc4164b5eb0338d14a6619"
    }
  }
}

在这个命令中,您通过提供一个具有ADMIN角色的人的用户名/密码来获得用户 Mark 在这种情况下,使用-uadmin@example.com:admin参数。

太好了。您正在使用 JDBC 通过使用 Spring Data REST 和 Spring Security 来查找用户!你可以让这个项目运行。

在待办事项应用中使用目录应用

是时候把这个目录 app 和 ToDo app 整合起来了。这很容易。

打开您的 ToDo 应用,让我们创建一个Person类。是的,我们需要一个Person类来保存足够的信息用于认证和授权。不需要有出生日期或任何其他信息(见清单 8-14 )。

package com.apress.todo.directory;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {

    private String email;
    private String password;
    private String role;
    private boolean enabled;
}

Listing 8-14com.apress.todo.directory.Person.java

清单 8-14 显示了Person类。这个类只有身份验证和授权过程所必需的字段。值得一提的是,调用 Directory app 会返回一个更完整的 JSON 对象。它必须匹配才能进行反序列化(使用 Jackson 库从 JSON 到 object),但是因为不需要额外的信息,所以这个类使用了@JasonIgnoreProperties(ignoreUnknown=true)注释来帮助匹配所需的字段。我认为这是一个很好的解耦类的方法。

注意

Java 中的一些序列化工具要求在同一个包中有相同的类并实现java.io.Serializable,这使得开发人员和客户更难管理和扩展。

接下来,创建保存目录应用信息的ToDoProperties类,如Uri(什么是地址和基 Uri)、UsernamePassword拥有ADMIN角色并有权访问 REST API 的人(参见清单 8-15 )。

package com.apress.todo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "todo.authentication")
public class ToDoProperties {

    private String findByEmailUri;
    private String username;
    private String password;

}

Listing 8-15com.apress.todo.config.ToDoProperties.java

清单 8-15 显示了ToDoProperties类;注意前缀是todo.authentication.*。接下来,修改ToDoSecurityConfig类。你可以注释整个类并复制清单 8-16 中的代码。

package com.apress.todo.config;

import com.apress.todo.directory.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@EnableConfigurationProperties(ToDoProperties.class)
@Configuration
public class ToDoSecurityConfig extends WebSecurityConfigurerAdapter {

    private final Logger log = LoggerFactory.getLogger(ToDoSecurityConfig.class);

    //Use this to connect to the Directory App
    private RestTemplate restTemplate;
    private ToDoProperties toDoProperties;
    private UriComponentsBuilder builder;

    public ToDoSecurityConfig(RestTemplateBuilder restTemplateBuilder, ToDoProperties toDoProperties){
        this.toDoProperties = toDoProperties;
        this.restTemplate = restTemplateBuilder.basicAuthorization(toDoProperties.getUsername(),toDoProperties.getPassword()).build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new UserDetailsService(){

            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

                try {
                    builder = UriComponentsBuilder
                  .fromUriString(toDoProperties.getFindByEmailUri())
                            .queryParam("email", username);

                    log.info("Querying: " + builder.toUriString());

                    ResponseEntity<Resource<Person>> responseEntity =
                    restTemplate.exchange(
                                  RequestEntity.get(URI.create(builder.toUriString()))
                                          .accept(MediaTypes.HAL_JSON)
                                          .build()
                            , new ParameterizedTypeReference<Resource<Person>>() {
                            });

                    if (responseEntity.getStatusCode() == HttpStatus.OK) {

                        Resource<Person> resource = responseEntity.getBody();
                        Person person = resource.getContent();

                        PasswordEncoder encoder =
                 PasswordEncoderFactories.createDelegatingPasswordEncoder();
                        String password = encoder.encode(person.getPassword());

                        return User
                  .withUsername(person.getEmail())
                  .password(password)
                  .accountLocked(!person.isEnabled())
                  .roles(person.getRole()).build();
                    }

                }catch(Exception ex) {
                    ex.printStackTrace();
                }
                throw new UsernameNotFoundException(username);
            }
        });

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .antMatchers("/","/api/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .and()

                .httpBasic();
    }

}

Listing 8-16com.apress.todo.config.ToDoSecurityConfig.java

清单 8-16 展示了新的ToDoSecurityConfig类。我们来分析一下。

  • WebSecurityConfigurerAdapter。此类覆盖了我们为应用自定义安全性所需的内容;但你已经知道了,对吧?

  • RestTemplate。这个助手类对目录应用端点,特别是/persons/search/findByEmailIgnoreCase Uri,进行 REST 调用。

  • UriComponentsBuilder。记住/persons/search/findByEmailIgnoreCase端点需要一个参数(email);那是由loadUserByUsername方法(username)提供的。

  • AuthenticationBuilder。认证提供了userDetailsService。在这段代码中,有一个UserDetailsService的匿名实现和loadUserByUsername方法的实现。这就是使用RestTemplate调用目录应用和端点的地方。

  • ResponseEntity。因为目录 app 响应是HAL+JSON,所以需要使用一个ResponseEntity来管理来自协议的所有资源。如果有HttpStatus.OK,很容易获得内容作为Person实例,并用它创建UserDetails

  • antMatchers。这个类像以前一样配置HttpSecurity,但是这一次它包含了一个antMatchers方法,该方法公开了由具有USER角色的有效人员访问的端点。

我们重用了目录应用中的相同技术。AuthenticationManager被配置为通过使用RestTemplate调用目录服务来提供UserDetails实例。目录应用用一个HAL+JSON协议响应,这就是为什么需要使用ResponseEntity来获得作为资源的人。

接下来,在application.properties文件中添加下面的todo.authentication.*属性。

# ToDo - Directory integration
todo.authentication.find-by-email-uri=http://localhost:8181/persons/search/findByEmailIgnoreCase
todo.authentication.username=admin@example.com
todo.authentication.password=admin

有必要指定搜索电子邮件端点的完整 Uri,以及具有ADMIN角色的人。

现在你已经准备好使用 ToDo 应用了。您可以使用浏览器或命令行。确定目录应用已启动并正在运行。运行端口 8080 中运行的 ToDo 应用。

您可以在终端窗口中执行以下命令。

$ curl localhost:8080/api/toDos -u mark@example.com:secret
{
  "_embedded" : {
    "toDos" :  {
      "description" : "Read a Book",
      "created" : "2018-08-17T07:42:44.136",
      "modified" : "2018-08-17T07:42:44.137",
      "completed" : true,

...
...

"profile" : {
      "href" : "http://localhost:8080/api/profile/toDos"
    }
  }
}

现在,您使用 Mark 进行身份验证和授权,他拥有USER角色。恭喜你!!您将自己的 JDBC 服务与 ToDo 应用集成在一起。

WebFlux 安全性

为了给 WebFlux 应用增加安全性,什么都不会改变。您需要添加spring-boot-starter-security依赖项,Spring Boot 会通过它的自动配置来完成剩下的工作。如果你想像我们之前那样定制,你唯一需要做的就是用ReactiveUserDetailsService(而不是UserDetailsService)或者用ReactiveAuthenticationManager(而不是AuthenticationManager)。请记住,现在您正在处理单一和通量反应流类型。

带有 OAuth2 的待办事项应用

有了 Spring Boot 和 Spring Security,OAuth2 比以往任何时候都容易。在本章的这一节,我们将使用 OAuth2 直接进入 ToDo 应用。我假设你知道 OAuth2,以及使用它作为与第三方提供商——如谷歌、脸书和 GitHub——直接进入你的应用的认证机制的所有好处。

让我们开始吧。从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。

  • 组:com.apress.todo

  • 神器:todo-oauth2

  • 名称:todo-oauth2

  • 包名:com.apress.todo

  • 依赖关系:WebSecurityLombokJPAREST RepositoriesH2MySQL

您可以选择 Maven 或 Gradle 作为项目类型。然后你可以按下生成项目按钮;这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 [8-8 )。

img/340891_2_En_8_Fig8_HTML.jpg

图 8-8

Spring 初始化 zr

如果您使用的是 Maven,将以下依赖项添加到您的pom.xml文件中。

<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

如果您正在使用 Gradle,请将以下依赖项添加到您的build.gradle中:

compile('org.springframework.security:spring-security-oauth2-client')
compile('org.springframework.security:spring-security-oauth2-jose')

可以想象,当 Spring Boot 看到spring-security-oauth2-client时,它会自动配置所有必要的 beans 来使用 OAuth2 安全性。重要的是要提到对包含 Spring Security 对 JOSE (JavaScript 对象签名和加密)框架支持的spring-security-oauth2-jose的需求。JOSE 框架旨在提供一种在各方之间安全转移索赔的方法。它是由一系列规范构建而成的:JSON Web Token (JWT)、JSON Web Signature (JWS)、JSON Web Encryption (JWE)和 JSON Web Key (JWK)。

接下来,您可以重用ToDo类和ToDoRepository接口(参见清单 8-17 和 8-18 )。

package com.apress.todo.domain;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Data
public class ToDo {

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;
    @NotNull
    @NotBlank
    private String description;

    @Column(insertable = true, updatable = false)
    private LocalDateTime created;
    private LocalDateTime modified;
    private boolean completed;

    public ToDo(){}
    public ToDo(String description){
        this.description = description;
    }

    @PrePersist
    void onCreate() {
        this.setCreated(LocalDateTime.now());
        this.setModified(LocalDateTime.now());
    }

    @PreUpdate
    void onUpdate() {
        this.setModified(LocalDateTime.now());
    }
}

Listing 8-17com.apress.todo.domain.ToDo.java

如你所见,一切都没变。它保持不变。

package com.apress.todo.repository;

import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;

public interface ToDoRepository extends CrudRepository<ToDo,String> { }

Listing 8-18com.apress.todo.repository.ToDoRepository.java

这个接口也是一样——没什么变化。我们来回顾一下application.properties

# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2-Console: http://localhost:8080/h2-console
# jdbc:h2:mem:testdb
spring.h2.console.enabled=true

什么都没变。嗯,我们将很快添加更多的属性。

现在重要的部分来了。您将使用 GitHub 对 ToDo 应用进行 OAuth2 身份验证。

在 GitHub 中创建 ToDo 应用

我假设您可能已经有了一个 GitHub 帐户;如果没有,您可以在 https://github.com 非常轻松地打开一个新的。您可以登录您的帐户,然后打开 https://github.com/settings/applications/new 。这是创建应用的地方。您可以使用下列值。

这对授权回调 URL 很重要,因为这是 Spring Security 的OAuth2LoginAuthenticationFilter期望与这个端点模式/login/oauth2/code/*一起工作的方式;当然,可以使用redirect-uri-template属性进行定制(见图 8-9 )。

img/340891_2_En_8_Fig9_HTML.jpg

图 8-9

GitHub 新 app: https://github.com/settings/applications/new

您可以单击“注册应用”按钮。之后,GitHub 会在您的应用中创建您需要的密钥(参见图 8-10 )。

img/340891_2_En_8_Fig10_HTML.jpg

图 8-10

客户端 ID 和客户端密钥

一旦你有了这个,复制客户端 id 和客户端密钥,并把它们和spring.security.oauth2.client.registration.*密钥一起添加到application.properties中。

# OAuth2
spring.security.oauth2.client.registration.todo.client-id=ac5b347117eb11705b70
spring.security.oauth2.client.registration.todo.client-secret=44abe272a15834a5390423e53b58f57c35647a98
spring.security.oauth2.client.registration.todo.client-name=ToDo App with GitHub Authentication
spring.security.oauth2.client.registration.todo.provider=github
spring.security.oauth2.client.registration.todo.scope=user
spring.security.oauth2.client.registration.todo.redirect-uri-template=http://localhost:8080/login/oauth2/code/github

spring.security.oauth2.client.registration接受包含必要键的地图,如client-idclient-secret

就是这样!!你不需要别的东西。您现在可以运行您的应用了。打开浏览器,指向 http://localhost:8080。你会得到一个重定向到 GitHub 的链接(见图 8-11 )。

img/340891_2_En_8_Fig11_HTML.jpg

图 8-11

http://localhost:8080

您可以点击链接,这将引导您完成登录过程,但使用 GitHub 认证机制(参见图 8-12 )。

img/340891_2_En_8_Fig12_HTML.jpg

图 8-12

GitHub 认证

您现在可以使用您的凭据登录。接下来,你会被重定向到另一个页面,在那里你需要授予 todo-app 使用联系信息的权限(参见图 8-13 )。

img/340891_2_En_8_Fig13_HTML.jpg

图 8-13

GitHub 授权流程

然后,您可以单击授权按钮,使用 ToDo REST API 返回到您的应用(参见图 8-14 )。

img/340891_2_En_8_Fig14_HTML.jpg

图 8-14

GitHub 授权流程后

恭喜你!!现在您知道使用 Spring Boot 和 Spring Security 将 OAuth2 与不同的提供商集成是多么容易了。

注意

你可以在 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 或者我的个人资源库 https://github.com/felipeg48/pro-spring-boot-2nd 找到这部分的解决方法。

摘要

在这一章中,您学习了使用 Spring Boot 进行安全保护的不同方法。您了解了通过添加spring-boot-security-starter依赖项来保护应用是多么容易。

您还了解了自定义和覆盖 Spring Boot 为您提供的 Spring Security 默认设置是很容易的。您可以使用spring.security.*属性,也可以使用WebSecurityConfigurerAdapter类对其进行定制。

您了解了如何使用 JDBC 并连接两个应用,其中一个作为身份验证和授权的安全机构。

最后,您了解了将 OAuth2 与第三方认证和授权提供商(如脸书、谷歌、GitHub 等)一起使用是多么容易。

在下一章中,我们开始与消息传递代理一起工作。