vlambda博客
学习文章列表

读书笔记《building-applications-with-spring-5-and-kotlin》春云

Spring Cloud

你好呀!你能相信我们已经完成了这本书的一半吗?我们要向您介绍的下一个主题是 Spring Cloud。 Spring Cloud 是 Spring 必须提供的最重要的功能之一。为什么我们需要它? Spring Cloud 为我们提供了构建分布式系统中一些最必要的模式的工具。通过使用 Spring Cloud,我们将在协调我们的分布式系统时避免样板代码。在本章中,我们将指导您实现一些最常用的配置。

在本章中,您将了解以下内容:

  • Microservice architecture
  • Microservice with Spring Cloud
  • Spring Cloud in practice
  • Updating API application
  • Securing Spring Cloud services

Microservice architecture versus SOA

SOA 代表面向服务的架构。提醒大家,简而言之,SOA可以通过以下几点来定义:

  • Services are autonomous
  • Their boundaries are explicit
  • Services share schemas and contracts (not class)
  • Service compatibility is based on the policy

在 SOA 中,可以将几个不同的服务(较小的应用程序)组合起来作为一个单一的大应用程序。这给我们的结论是,通过使用 SOA,我们实现了软件的模块化。 SOA 中的服务通过描述它们的协议传递和解析消息。

基于此,我们可以从 SOA 的角度来解释微服务。微服务代表了 SOA 的解释,用于构建分布式系统。微服务架构中的服务是相互通信的进程。您一定已经注意到,微服务没有统一的共同定义。因此,我们将重点介绍以下最常被提及的特征和原则:

  • Delicate interfaces for deploying services independently
  • Business-driven development, for example, domain-driven design
  • Polyglot programming
  • Persistence
  • Lightweight container deployment
  • IDEAL cloud application architectures
  • Decentralized continuous delivery
  • Holistic service monitoring for DevOps

Understanding microservice architectures

让我们更多地关注微服务架构。它是现代企业应用程序开发中最常用的架构。由于它是可扩展的,因此它被认为是此类开发的最佳方法之一。

理解微服务的第一个关键点是整个系统必须分解成多个独立的应用程序。这样做有什么好处?每个应用程序(服务)都可以根据需要轻松独立地部署、维护和重新部署。

每个微服务都有一个单一的职责,这是它的主要上下文。例如,开发团队可以独立专注于某些上下文和功能集的开发。微服务接收请求,处理它们,并将响应发送到信息流经的管道。

要理解的重点之一是微服务可能会遇到某些故障。由于它们相互依赖以及处理过的数据,处理这些故障会增加它们的复杂性。

所以,让我们总结一下!微服务架构使用服务作为组件。每个服务都是围绕特定的业务环境组织的。它们具有智能端点,但信息流机制非常简单。治理是去中心化的,数据管理也是去中心化的。

Microservices with Spring Cloud

我们将把我们的 API 视为一个拼图。假设它将是微服务架构中的服务之一。我们想要实现的是让在任何分布式环境中轻松工作成为可能。 Spring Cloud 构建在 Spring Boot 之上,并提供了一组库,这些库在添加到类路径时可以扩展我们的应用程序的能力。

Spring Cloud 提供以下功能:

  • Distributed and versioned configuration: Spring Cloud configuration provides server and client-side support for configuration externalization. We define the configuration server as the central place to manage external properties for applications across all environments.
  • Service registration and discovery: We need a way for all of our servers to be able to find each other. Spring Cloud makes this possible and easy to do.
  • Routing and filtering: With routing and filtering, the Spring Cloud feature microservice application can work as a reverse proxy. Microservices can forward requests to the other service application. With the same microservice, it's also possible to apply proper filtering so that maximal flexibility is achieved.
  • Calls service-to-service: Spring Cloud makes it possible to easily establish communication between microservices. For this purpose, Spring Cloud offers direct support for Eureka! Besides this support, Spring Cloud supports the following integrations, too:
    • Hystrix
    • Zuul
    • Archaius and many others
  • Load balancing: Every modern system requires load balancing—especially systems with big traffic. Thanks to Spring Cloud, we can implement a microservice application that uses Netflix Ribbon and Spring Cloud Netflix to provide client-side load balancing in calls to another microservice.
  • Circuit breakers: Spring Cloud supports circuit breaker pattern implementation. So, with the use of the circuit breaker pattern, we can allow a microservice to continue operating even when a related service is failing! By doing this, we prevent the failure from cascading.
  • Leadership election and cluster state: Leadership election allows microservices to work together with other microservices so that they can coordinate a cluster leadership via a third-party system. A leader can then be used to provide a global state or global ordering with high availability.
  • Distributed messaging: Spring Cloud Bus connects our microservices with a lightweight message broker. Thanks to this, we can broadcast state changes or other instructions. With this, huge flexibility is achieved without the need for boilerplate code.

这些特性中的每一个都代表任何严肃的企业系统需要的要求。在深入探讨最重要的部分之前,我们将进行简要说明,以便您了解可以创建的内容。

Spring Cloud in practice

最后,是时候向您展示如何使用 Spring Cloud。我们将扩展我们的项目并介绍最常用的 Spring Cloud 组件的实现。为了能够使用它,请使用 Spring Cloud 依赖项扩展 build.gradle 配置:

buildscript { 
    ext { 
        kotlinVersion = '1.1.60' 
        springBootVersion = '2.0.0.M6' 
    } 
    ... 
} 
 
... 
 
repositories { 
    ... 
} 
 
dependencies { 
    ... 
    // compile 'org.springframework.cloud:spring-cloud-config-server' 
    ... 
} 
 
dependencyManagement { 
    imports { 
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:Finchley.M4" 
    } 
} 

如您所见,我们将 Spring Boot 版本 2.0.0.M4 升级为 2.0.0.M6。我们还添加了(现在,在评论下)spring-cloud-config-server 依赖项和 spring-cloud-dependencies:Finchley 的 dependencyManagement 导入。 M4

构建并运行您的项目以确保没有任何损坏。我们现在准备开发一些严肃的 Spring Cloud 东西!

我们要支持的第一个 Spring Cloud 特性将是分布式配置。为此,我们必须定义以下内容:

  • Configuration server: Here, we will define our configuration application responsible for providing configurations to all other applications (microservices), and then we will connect to it to obtain proper application configuration. We will present to you a simple implementation so that you can understand this concept.
  • Discovery: Here, we need a mechanism so that all of our servers are be able to find each other. We will resolve this by running the Eureka discovery server.
  • Gateway: To resolve the problem of clients accessing all of our defined applications, we will create a gateway. The gateway will behave as a reverse proxy, managing requests from clients to our servers.

Configuration server

让我们为配置服务器创建一个 Spring 项目。我们将定义一个带有 Spring Cloud 依赖项的空白应用程序。稍后,我们将添加代码,代表服务器配置实现。该应用程序将属于以下上下文:

com.journaler.config

确保您的应用程序已定义,例如以下文件:

  • The build.gradle file is as follows:
buildscript { 
    ext { 
        kotlinVersion = '1.1.60' 
        springBootVersion = '2.0.0.M6' 
    } 
    repositories { 
        mavenCentral() 
        maven { url "https://repo.spring.io/snapshot" } 
        maven { url "https://repo.spring.io/milestone" } 
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 
    } 
} 
 
apply plugin: 'kotlin' 
apply plugin: 'kotlin-spring' 
apply plugin: 'eclipse' 
apply plugin: 'org.springframework.boot' 
apply plugin: 'io.spring.dependency-management' 
 
group = 'com.journaler.config' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = 1.8 
 
compileKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
compileTestKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "https://repo.spring.io/snapshot" } 
    maven { url "https://repo.spring.io/milestone" } 
} 
 
dependencies { 
    compile 'org.springframework:spring-context' 
    compile 'org.springframework:spring-aop' 
    compile 'org.springframework.boot:spring-boot-starter' 
    compile 'org.springframework.boot:spring-boot-starter-web' 
    compile 'org.springframework.boot:spring-boot-starter-actuator' 
    compile 'org.springframework.cloud:spring-cloud-config-server' 
    compile 'org.springframework.cloud:spring-cloud-starter-eureka' 
    compile 'org.springframework:spring-web' 
    compile 'org.springframework:spring-webmvc' 
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}" 
    compile "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" 
    testCompile 'org.springframework.boot:spring-boot-starter-test' 
} 
 
dependencyManagement { 
    imports { 
        mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT" 
    } 
} 
  • The application.properties file is as follows:
spring.application.name= config 
server.port= 9001 
logging.level.root=INFO 
logging.level.com.journaler.api=DEBUG 
logging.level.org.springframework.jdbc=ERROR 
 
endpoints.health.enabled=true 
endpoints.trace.enabled=true 
endpoints.info.enabled=true 
endpoints.metrics.enabled=true 
  • Create ConfigApplication.kt under the com.journaler.config package:
package com.journaler.config 
 
import org.springframework.boot.SpringApplication 
import org.springframework.boot.autoconfigure.SpringBootApplication 
 
@SpringBootApplication 
class ConfigApplication 
 
fun main(args: Array<String>) { 
    SpringApplication.run(ConfigApplication::class.java, *args) 
} 

构建并运行它。该应用程序将作为在端口 9001 上运行的 localhost 应用程序启动。您会记得,我们的 API 在端口 9000 上运行。

让我们继续!更新应用程序类以支持 Spring Cloud 配置:

import org.springframework.cloud.config.server.EnableConfigServer

@SpringBootApplication @EnableConfigServer class ConfigApplication fun main(args: Array<String>) { SpringApplication.run(ConfigApplication::class.java, *args) }

现在,使用以下扩展 application.properties

spring.cloud.config.server.git.uri=file://${user.home}/Projects/Building-Applications-with-Spring-5-and-Kotlin-Config-Repo 

我们刚刚添加的行定义了 Git 存储库的位置,我们将在其中找到属性源。要准备好 Git 存储库,请导航到您的 home 目录并创建一个 Projects 目录,其中包含一个名为 Building-Applications-with-Spring-5-and- 的子目录Kotlin-Config-Repo。要初始化您的存储库,请执行以下命令:

$ git init . 

存储库已准备就绪。构建并运行您的应用程序以确保目前一切正常。

您也可以克隆现有的配置存储库!这是您的 application.properties 需要的一些行的示例:

spring.cloud.config.server.git.uri=ssh://some_domain/config-repo 
spring.cloud.config.server.git.clone-on-start=true 
security.user.name=git_username 
security.user.password=git_password 

Discovery

接下来我们要做的是为我们的服务器提供一种机制,以便它们能够发现彼此。 Eureka 发现服务器将成为此需求的解决方案。我们将为我们的应用程序设置集中式注册表。流程是这样的:我们的每一台服务器都会联系发现服务器实例并注册它的地址。由于这一点,所有其他人都可以与它交流。

设置新的 Spring Cloud 应用程序,类似于配置应用程序的操作。确保它是这样定义的:

  • The build.gradle file configuration:
buildscript { 
    ext { 
        kotlinVersion = '1.1.60' 
        springBootVersion = '2.0.0.M6' 
    } 
    repositories { 
        mavenCentral() 
        maven { url "https://repo.spring.io/snapshot" } 
        maven { url "https://repo.spring.io/milestone" } 
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 
    } 
} 
 
apply plugin: 'kotlin' 
apply plugin: 'kotlin-spring' 
apply plugin: 'eclipse' 
apply plugin: 'org.springframework.boot' 
apply plugin: 'io.spring.dependency-management' 
 
group = 'com.journaler.discovery' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = 1.8 
 
compileKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
compileTestKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "https://repo.spring.io/snapshot" } 
    maven { url "https://repo.spring.io/milestone" } 
} 
 
dependencies { 
    compile 'org.springframework:spring-context' 
    compile 'org.springframework:spring-aop' 
    compile 'org.springframework.boot:spring-boot-starter' 
    compile 'org.springframework.boot:spring-boot-starter-web' 
    compile 'org.springframework.boot:spring-boot-starter-actuator' 
    compile 'org.springframework.cloud:spring-cloud-starter-config' 
    compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' 
    compile 'org.springframework:spring-web' 
    compile 'org.springframework:spring-webmvc' 
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}" 
    compile "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" 
    testCompile 'org.springframework.boot:spring-boot-starter-test' 
} 
 
dependencyManagement { 
    imports { 
        mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT" 
    } 
} 

向我们介绍的最重要的依赖项包括以下内容:

compile 'org.springframework.cloud:spring-cloud-starter-config' 
compile 'org.springframework.cloud:spring-cloud-starter-eureka-server'
mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT"

我们将定义 bootstrap.properties 文件,而不是在应用程序资源中使用传统的 application.properties 文件,该文件将定义要定位的配置服务器,它的端口,和配置名称:

spring.cloud.config.name=discovery 
spring.cloud.config.uri=http://localhost:9001 

我们需要的最后一件事是 Application 类,使用以下内容定义 DiscoveryApplication.kt

package com.journaler.discovery 
 
import org.springframework.boot.SpringApplication 
import org.springframework.boot.autoconfigure.SpringBootApplication 
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer 
 
@SpringBootApplication 
@EnableEurekaServer 
class DiscoveryApplication 
 
fun main(args: Array<String>) { 
    SpringApplication.run(DiscoveryApplication::class.java, *args) 
} 

如您所见,我们所要做的就是通过添加 @EnableEurekaServer 注释来启用 Eureka 服务器。

在我们运行任何东西之前,在您的 Git 存储库中创建一个名为 discovery.properties 的配置文件(我们创建的文件必须与 spring.application.name 属性值同名):

spring.application.name= discovery 
server.port= 9002 
logging.level.root=INFO 
logging.level.com.journaler.api=DEBUG 
logging.level.org.springframework.jdbc=ERROR 
 
endpoints.health.enabled=true 
endpoints.trace.enabled=true 
endpoints.info.enabled=true 
endpoints.metrics.enabled=true 
 
eureka.instance.hostname=localhost 
 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 
eureka.client.register-with-eureka=false 
eureka.client.fetch-registry=false 

这个配置代表什么?首先,它包含了我们通常提供的标准信息。此外,还有以下内容:

eureka.instance.hostname=localhost 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 
eureka.client.register-with-eureka=false 
eureka.client.fetch-registry=false 

这样,我们就告诉服务器它在默认区域中运行,这意味着我们正在匹配配置客户端的区域设置。需要注意的是,我们还定义了服务器不会对任何其他发现实例执行的行为!

现在,保存所有工作,首先构建并运行配置服务器,然后是发现服务器。如果您查看发现应用程序的日志,您会注意到这是有效的:

  • The upper part of the printed log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • The lower part of the printed log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云

Gateway

我们分布式系统的下一步是定义网关。网关用于允许所有响应来自单个主机。让我们创建另一个 Spring Cloud 应用程序。确保它在以下文件中定义:

  • The build.gradle file is as follows:
buildscript { 
    ext { 
        kotlinVersion = '1.1.60' 
        springBootVersion = '2.0.0.M6' 
    } 
    repositories { 
        mavenCentral() 
        maven { url "https://repo.spring.io/snapshot" } 
        maven { url "https://repo.spring.io/milestone" } 
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 
    } 
} 
 
apply plugin: 'kotlin' 
apply plugin: 'kotlin-spring' 
apply plugin: 'eclipse' 
apply plugin: 'org.springframework.boot' 
apply plugin: 'io.spring.dependency-management' 
 
group = 'com.journaler.gateway' 
version = '0.0.1-SNAPSHOT' 
sourceCompatibility = 1.8 
 
compileKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
compileTestKotlin { 
    kotlinOptions.jvmTarget = "1.8" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "https://repo.spring.io/snapshot" } 
    maven { url "https://repo.spring.io/milestone" } 
} 
 
dependencies { 
    compile 'org.springframework:spring-context' 
    compile 'org.springframework:spring-aop' 
    compile 'org.springframework.boot:spring-boot-starter' 
    compile 'org.springframework.boot:spring-boot-starter-web' 
    compile 'org.springframework.boot:spring-boot-starter-actuator' 
    compile 'org.springframework.cloud:spring-cloud-starter-config' 
    compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' 
    compile 'org.springframework.cloud:spring-cloud-starter-zuul' 
    compile 'org.springframework:spring-web' 
    compile 'org.springframework:spring-webmvc' 
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}" 
    compile "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}" 
    testCompile 'org.springframework.boot:spring-boot-starter-test' 
} 
 
dependencyManagement { 
    imports { 
        mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT" 
    } 
} 
  • The bootstrap.properties file is as follows:
spring.cloud.config.name=gateway 
spring.cloud.config.discovery.service-id=config 
spring.cloud.config.discovery.enabled=true 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 
  • Under the com.journaler.gateway package, create the Application class:
package com.journaler.gateway 
 
import org.springframework.boot.SpringApplication 
import org.springframework.boot.autoconfigure.SpringBootApplication 
import org.springframework.cloud.netflix.eureka.EnableEurekaClient 
import org.springframework.cloud.netflix.zuul.EnableZuulProxy 
 
@SpringBootApplication 
@EnableZuulProxy 
@EnableEurekaClient 
class GatewayApplication 
 
fun main(args: Array<String>) { 
    SpringApplication.run(GatewayApplication::class.java, *args) 
} 

然后,通过添加以下行来更新配置服务器应用程序的 application.properties

eureka.client.region = default 
eureka.client.registryFetchIntervalSeconds = 5 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 

这将允许我们在配置服务器和发现服务器之间建立通信,以便我们可以完成这部分的实现。

那么,我们刚刚做了什么?让我们来看看它。首先,我们通过添加以下内容满足了使用 Spring Cloud 作为代理的必要依赖项:

compile 'org.springframework.cloud:spring-cloud-starter-config' 
compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' 
compile 'org.springframework.cloud:spring-cloud-starter-zuul' 

mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT"

然后,我们将网关应用程序定义为配置和发现客户端和网关代理。我们通过使用 @EnableEurekaClient@EnableZuulProxy 注释来实现这一点。

最重要的部分是属性定义。 bootstrap.properties 配置告诉我们的应用程序与发现服务器对话以获取其配置。其余配置在我们的 Git 存储库下定义。 zuul.routes 属性的目的是基于 URL 匹配器路由请求。我们告诉 Zuul 将来自 '/journaler/**' 路径的任何请求路由到具有 journaler 的 spring.application.name 属性的应用程序。然后 Zuul 将执行查找。使用应用程序名称的发现服务器中的主机将被找到,请求将被转发给它。

最后,我们将运行所有应用程序。启动配置和发现服务器(应用程序)。然后,运行网关应用程序。请注意,在您的日志中,您将看到类似以下内容:

  • The upper part of the log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • The lower part of the log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云

Updating the API application

Journaler API 应用程序尚未更新为分布式配置系统的一部分。我们现在将重新配置它!它不需要太多的工作来实现这一点。我们要做的第一件事是更新我们的依赖项。打开 build.gradle 配置,确保你的依赖如下:

... 
dependencies { 
    ... 
    compile 'org.springframework.cloud:spring-cloud-starter-config' 
    compile 'org.springframework.cloud:spring-cloud-starter-eureka' 
    ... 
} 
 
dependencyManagement { 
    imports { 
        mavenBom "org.springframework.cloud:spring-cloud-netflix:1.4.1.BUILD-SNAPSHOT" 
    } 
} 

当依赖关系得到满足时,我们可以更新我们的 Application 类。打开 ApiApplication 类并通过添加适当的注释来修改它,以便应用程序是 Eureka 服务器客户端:

package com.journaler.api 
 
import org.springframework.boot.SpringApplication 
import org.springframework.boot.autoconfigure.SpringBootApplication 
import org.springframework.cloud.netflix.eureka.EnableEurekaClient 
 
@SpringBootApplication 
@EnableEurekaClient 
class ApiApplication 
 
fun main(args: Array<String>) { 
    SpringApplication.run(ApiApplication::class.java, *args) 
} 

在 Git 存储库中,创建一个 journaler.properties 配置文件:

spring.application.name= journaler 
server.port= 9000 
logging.level.root=INFO 
logging.level.com.journaler.api=DEBUG 
logging.level.org.springframework.jdbc=ERROR 
 
endpoints.health.enabled=true 
endpoints.trace.enabled=true 
endpoints.info.enabled=true 
endpoints.metrics.enabled=true 
 
spring.datasource.url= 
jdbc:mysql://localhost/journaler_api?useSSL=false&useUnicode=true&characterEncoding=utf-8 
 
spring.datasource.username=root 
spring.datasource.password=localInstance2017 
spring.datasource.tomcat.test-on-borrow=true 
spring.datasource.tomcat.validation-interval=30000 
spring.datasource.tomcat.validation-query=SELECT 1 
spring.datasource.tomcat.remove-abandoned=true 
spring.datasource.tomcat.remove-abandoned-timeout=10000 
spring.datasource.tomcat.log-abandoned=true 
spring.datasource.tomcat.max-age=1800000 
spring.datasource.tomcat.log-validation-errors=true 
spring.datasource.tomcat.max-active=50 
spring.datasource.tomcat.max-idle=10 
 
spring.jpa.hibernate.ddl-auto=update 
 
eureka.client.region = default 
eureka.client.registryFetchIntervalSeconds = 5 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 

最后,从资源目录中删除 application.properties。相反,使用以下配置创建一个 bootstrap.properties 文件:

spring.cloud.config.name=journaler 
spring.cloud.config.discovery.service-id=config 
spring.cloud.config.discovery.enabled=true 
 
eureka.client.serviceUrl.defaultZone=http://localhost:9002/eureka/ 

现在,我们准备好运行并尝试一切!一一启动所有应用程序:

  • Configuration
  • Discovery
  • Gateway
  • Journaler API application

观察 Journaler API 日志。您将获得与此类似的日志输出:

  • The upper part of the log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • The lower part of the log:
读书笔记《building-applications-with-spring-5-and-kotlin》春云

当 Journaler API 应用程序启动时,尝试一些现有的 API 调用:

  • [ POST ] http://localhost:9000/login. The result is as follows:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • [ GET ] http://localhost:9000/notes. The result is as follows:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
The GET API call for notes

现在,通过运行在端口 9003 上的网关服务器尝试相同的 API 调用:

  • [ POST ] http://localhost:9003/journaler/login. The result is as follows:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
The POST API call for login through the gateway server
  • [ GET ] http://localhost:9003/journaler/notes. The result is as follows:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
The GET API call for notes through the gateway server

如您所见,通过网关服务器(应用程序)执行的笔记的 GET 方法向我们返回了未经授权的响应。这是因为应用程序不知道我们获得的会话。为此,我们必须为此设置会话共享。共享会话使我们能够在网关应用程序上登录系统用户,然后将身份验证传播到依赖它的其他服务。

Securing Spring Cloud services

在本节中,我们将保护我们的 Spring Cloud 配置,并且为了简单起见,暂时将 Journaler API 从安全限制中释放出来。我们将演示保护微服务的基本原则,并逐步指导您实现这一目标。

我们的每个模块都必须支持 Spring Security 和 Spring 会话。为此,使用 Spring Security 和 Spring 会话支持依赖项扩展每个 build.gradle 配置:

...  
dependencies { 
    compile 'org.springframework.boot:spring-boot-starter-security' 
} 
...  

我们会将所有会话存储在内存中。为此,我们将使用 Redis 内存数据库。我们必须通过扩展具有以下依赖关系的 build.gradle 来扩展我们的每个应用程序以支持它:

...  
dependencies { 
    compile 'org.springframework.boot:spring-boot-starter-data-redis' 
} 
...  

现在,我们准备添加会话配置。在定义主应用程序类的同一包级别中添加会话配置类:

package com.journaler.api 
 
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession 
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer 
 
@EnableRedisHttpSession 
class SessionConfiguration : AbstractHttpSessionApplicationInitializer() 

对以下应用程序执行相同操作:发现、网关和 Journaler API。您将拥有三个具有相同实现的类!完成会话配置类后,通过添加以下代码扩展您在 Git 存储库中定义的应用程序配置(适用于所有三个应用程序):

spring.redis.host=localhost  
spring.redis.port=6379 

这将为每个应用程序提供正确的 Redis 配置。

对于 Configuration 应用程序,使用以下内容扩展其 application.properties

eureka.client.region = default 
eureka.client.registryFetchIntervalSeconds = 5 
 
eureka.client.serviceUrl.defaultZone= 
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/ 
 
security.user.name=configAdmin 
security.user.password=configPassword12345 
security.user.role=SYSTEM 
 
spring.session.store-type=redis 

通过这样做,我们将确保应用程序通过发现登录。我们也必须保护发现服务。使用如下定义的 WebSecurityConfiguration 类创建包安全性:

package com.journaler.discovery.security 
 
import org.springframework.security.config.annotation.web.builders.HttpSecurity 
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.context.annotation.Configuration 
import org.springframework.core.annotation.Order 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 
import org.springframework.security.config.http.SessionCreationPolicy 
 
 
@Configuration 
@EnableWebSecurity 
@Order(1) 
class SecurityConfig : WebSecurityConfigurerAdapter() { 
 
    @Autowired 
    fun configureGlobal(auth: AuthenticationManagerBuilder) { 
        auth 
                .inMemoryAuthentication() 
                .withUser("discoveryAdmin") 
                .password("discoveryPassword12345") 
                .roles("SYSTEM") 
    } 
 
    override fun configure(http: HttpSecurity) { 
        http 
                .sessionManagement() 
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) 
                .and().requestMatchers().antMatchers("/eureka/**") 
                .and().authorizeRequests().antMatchers("/eureka/**") 
                .hasRole("SYSTEM").anyRequest().denyAll().and() 
                .httpBasic().and().csrf().disable() 
    } 
} 

这将匹配 Configuration 应用程序属性中的用户名和密码组合。我们必须注意,我们使用了 @Order 注释,以便我们可以告诉 Spring 将此配置用作其第一优先级。使用 sessionCreationPolicy() 方法和 ALWAYS 参数,我们定义会话将在每次用户登录尝试时创建。

我们要做的下一件事是告诉发现应用程序有关用于登录配置应用程序的凭据。扩展其 bootstrap.properties 配置,使其如下所示:

spring.cloud.config.name=discovery 
spring.cloud.config.uri=http://localhost:9001 
spring.cloud.config.username=configAdmin 
spring.cloud.config.password=configPassword12345 

最后,从我们的 Git 存储库中修改 discovery.properties 配置:

...  
eureka.client.serviceUrl.defaultZone= 
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/ 
eureka.client.register-with-eureka=false 
eureka.client.fetch-registry=false 
...  

我们使用发现应用程序的凭据扩展了配置。

由于我们已经保护了配置和发现应用程序,是时候为我们的网关应用程序做同样的事情了。创建一个包含 WebSecurityConfiguration 类的 security 包:

package com.journaler.gateway.security 
 
import org.springframework.security.config.annotation.web.builders.HttpSecurity 
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 
import org.springframework.beans.factory.annotation.Autowired 
import org.springframework.context.annotation.Configuration 
import org.springframework.core.annotation.Order 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 
 
@Configuration 
@EnableWebSecurity 
@Order(1) 
class SecurityConfig : WebSecurityConfigurerAdapter() { 
 
    @Autowired 
    fun configureGlobal(auth: AuthenticationManagerBuilder) { 
        auth 
                .inMemoryAuthentication() 
                .withUser("user") 
                .password("12345") 
                .roles("USER") 
                .and() 
                .withUser("admin") 
                .password("12345") 
                .roles("ADMIN") 
    } 
 
    override fun configure(http: HttpSecurity) { 
        http 
                .authorizeRequests() 
                .antMatchers("/journaler/**") 
                .permitAll() 
                .antMatchers("/eureka/**").hasRole("ADMIN") 
                .anyRequest().authenticated() 
                .and() 
                .formLogin() 
                .and() 
                .logout().permitAll() 
                .logoutSuccessUrl("/journaler/**").permitAll() 
                .and() 
                .csrf().disable() 
    } 
} 

我们定义了两个具有两个角色的用户:普通用户和管理员用户。我们还使用表单登录定义了安全过滤器。现在,切换到网关的会话配置类并更新它:

package com.journaler.gateway 
 
import org.springframework.session.data.redis.RedisFlushMode 
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession 
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer 
 
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE) 
class SessionConfiguration : AbstractHttpSessionApplicationInitializer() 

会话上发生的任何更改都将立即保留!最后,我们将添加功能,以便我们可以在登录后转发身份验证令牌。创建一个名为 SessionFilter 的新类:

package com.journaler.gateway 
 
import com.netflix.zuul.ZuulFilter 
import com.netflix.zuul.context.RequestContext 
import org.springframework.stereotype.Component 
import org.springframework.session.SessionRepository 
import org.springframework.beans.factory.annotation.Autowired 
 
@Component 
class SessionFilter : ZuulFilter() { 
 
    @Autowired 
    lateinit var repository: SessionRepository<*> 
 
    override fun shouldFilter(): Boolean { 
        return true 
    } 
 
    override fun run(): Any? { 
        val context = RequestContext.getCurrentContext() 
        val httpSession = context.request.session 
        val session = repository?.getSession(httpSession.id) 
        context.addZuulRequestHeader("Cookie", "SESSION=" + httpSession.id) 
        println("Session ID available: ${session.id}") 
        return null 
    } 
 
    override fun filterType(): String { 
        return "pre" 
    } 
 
    override fun filterOrder(): Int { 
        return 0 
    } 
} 

我们刚刚定义的过滤器将接受一个请求并将会话密钥作为 cookie 添加到请求的标头中。

在我们更新 Journaler API 应用程序之前,我们需要进行一些小的更新。更新 bootstrap.properties 配置:

spring.cloud.config.name=gateway 
spring.cloud.config.discovery.service-id=config 
spring.cloud.config.discovery.enabled=true 
spring.cloud.config.username=configAdmin 
spring.cloud.config.password=configPassword12345 
eureka.client.serviceUrl.defaultZone= 
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/ 

此外,在我们的 Git 存储库中更新 gateway.properties

spring.application.name=gateway 
server.port=9003 
 
eureka.client.region = default 
eureka.client.registryFetchIntervalSeconds = 5 
 
management.security.sessions=always
spring.redis.host=localhost 
spring.redis.port=6379 
 
zuul.routes.journaler.path=/journaler/** 
zuul.routes.journaler.sensitive-headers=Set-Cookie,Authorization 
hystrix.command.journaler.execution.isolation.thread.timeoutInMilliseconds=600000 
 
zuul.routes.discovery.path=/discovery/** 
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization 
zuul.routes.discovery.url=http://localhost:9002 
hystrix.command.discovery.execution.isolation.thread.timeoutInMilliseconds=600000 

我们定义会话管理将始终生成会话。我们还移动了 Redis 的配置,以便在会话管理之后定义它。

我们可以将 gateway.properties 文件中的 serviceUrl.defaultZone 属性删除到我们的配置 Git 存储库中。该值在引导文件中重复。

提交所有具有配置更改的 Git 存储库,以便这些更改在我们的下一次运行中生效!

我们非常接近完成对 Journaler API 的最后润色!在我们这样做之前,让我们运行配置、发现和网关应用程序。为了能够运行它们,我们将安装 Redis 服务器。我们将在 macOS 上使用 Homebrew 进行安装过程。这非常简单易行。打开终端并执行以下命令:

$ brew install redis 

一段时间后,将安装 Redis。现在,通过发出以下命令启动它:

$ redis-server 

Redis 服务器将启动:

读书笔记《building-applications-with-spring-5-and-kotlin》春云

对于任何其他操作系统,请按照 Redis 官方主页的说明进行操作:https://redis.io/

现在,一一运行配置、发现和网关应用程序。一切都应该正常工作!

从 Journaler API 应用程序中打开 WebSecurityConfiguration 类并进行更改。新的实现应该是这样的:

package com.journaler.api.security 
 
import org.springframework.context.annotation.Configuration 
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter 
import org.springframework.security.config.annotation.web.builders.HttpSecurity 
import org.springframework.core.annotation.Order 
 
@Configuration 
@EnableWebSecurity 
@Order(1) 
class SecurityConfig : WebSecurityConfigurerAdapter() { 
 
    override fun configure(http: HttpSecurity) { 
        http 
                .httpBasic().disable().authorizeRequests() 
                .antMatchers("/notes").permitAll() 
                .antMatchers("/notes/**").permitAll() 
                .antMatchers("/todos").permitAll() 
                .antMatchers("/todos/**").permitAll() 
                .anyRequest() 
                .authenticated() 
                .and() 
                .csrf().disable() 
    } 
} 

请删除所有不再使用的与用户实体相关的类和其他与安全相关的类!

现在,更新 bootstrap.properties 配置:

spring.cloud.config.name=journaler 
spring.cloud.config.discovery.service-id=config 
spring.cloud.config.discovery.enabled=true 
 
spring.cloud.config.username=configAdmin 
spring.cloud.config.password=configPassword12345 
eureka.client.serviceUrl.defaultZone= 
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/ 

在我们的 Git 存储库中更新并提交 journaler.properties 文件:

... 
management.security.sessions=never 
... 

我们还删除了 serviceUrl.defaultZone 属性,因为它已经在 boots.properties 配置中定义。

由于我们在开发过程中更新了我们的依赖关系,我们没有注意到我们现在遇到了请求类和 DTO 的问题。我们必须为每一个引入一个默认的空构造函数。让我们快速执行此操作:

  • NoteDTO:
data class NoteDTO( 
        var title: String, 
        var message: String, 
        var location: String = "" 
) { 
    constructor() : this("", "", "") 
    ...  
} 
  • TodoDTO:
data class TodoDTO( 
        var title: String, 
        var message: String, 
        var schedule: Long, 
        var location: String = "" 
) { 
    ...  
    constructor() : this("", "", -1, "") 
} 
  • NoteFindByTitleRequest:
package com.journaler.api.controller 
 
data class NoteFindByTitleRequest(val title: String) { 
 
    constructor() : this("") 
 
} 
  • TodoLaterThanRequest:
package com.journaler.api.controller 
 
import java.util.* 
 
data class TodoLaterThanRequest(val date: Date? = null) { 
 
    constructor() : this(null) 
 
} 
  • TodoService:
@Service("Todo service") 
class TodoService { 
    ... 
    fun getScheduledLaterThan(date: Date?): Iterable<TodoDTO> { 
        date?.let { 
            return repository.findScheduledLaterThan(date.time).map { it -> TodoDTO(it) } 
        } 
        return listOf() 
    } 
} 

如果您需要从数据库中删除所有表,请运行 Journaler API 应用程序。让我们尝试几个以网关应用程序为目标的调用,并确认所有调用都被重定向:

  • [ PUT ] http://localhost:9003/journaler/notes: Execute the call a couple of times:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • [ GET ] http://localhost:9003/journaler/notes:
读书笔记《building-applications-with-spring-5-and-kotlin》春云
The GET API call for notes through the gateway application
  • [ POST ] http://localhost:9003/journaler/notes/by_title with the title parameter "My 1st note":
读书笔记《building-applications-with-spring-5-and-kotlin》春云
  • [ POST ] http://localhost:9003/journaler/notes/by_title with the title parameter "My 3rd note":
读书笔记《building-applications-with-spring-5-and-kotlin》春云
The POST API call for notes through the gateway application

Summary

Spring Cloud 是一个非常强大的框架!尽管我们在本章中走了很长一段路,但我们只触及了 Spring Cloud 的基础知识。了解可以实现的目标以及如何开始这样做很重要。如果你觉得这太复杂了,请从本章的开头重新开始,慢慢地做所有的练习。如果您了解所有这些,那么接下来您应该做的就是让 Spring 会话可以传播到 Journaler API,并通过我们在练习结束时删除的完整 Spring Security 配置来使用它。花点时间,尽可能多地进行研究和编码!在下一章中,我们将研究 Reactor 项目,并尝试演示如何以及为什么应该使用响应式!