使用Spring Cloud Gateway保护反应式微服务

朋友不允许朋友写用户身份验证。 厌倦了管理自己的用户? 立即尝试Okta的API和Java SDK。 在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护。

所以你想完全反应,是吗? 大! 反应式编程是使您的应用程序更高效的一种越来越流行的方式。 响应式应用程序异步调用响应,而不是调用资源并等待响应。 这使他们可以释放处理能力,仅在必要时执行处理,并且比其他系统更有效地扩展。

Java生态系统在反应框架中占有相当大的份额,其中包括Play框架,Ratpack,Vert.x和Spring WebFlux。 像反应式编程一样,微服务架构可以帮助大型团队快速扩展,并且可以使用上述任何出色的框架进行构建。

今天,我想向您展示如何使用Spring Cloud Gateway,Spring Boot和Spring WebFlux构建反应性微服务架构。 我们将利用Spring Cloud Gateway,因为API网关通常是云原生微服务体系结构中的重要组件,为所有后端微服务提供了聚合层。

本教程将向您展示如何使用REST API构建微服务,该API返回新车列表。 您将使用Eureka进行服务发现,并使用Spring Cloud Gateway将请求路由到微服务。 然后,您将集成Spring Security,以便只有经过身份验证的用户才能访问您的API网关和微服务。

先决条件 : HTTPie (或cURL), Java 11+和Internet连接。

Spring Cloud Gateway与Zuul

Zuul是Netflix的API网关。 Zuul于2013年首次发布,最初并不具有反应性,但Zuul 2是彻底的重写,使其具有反应性。 不幸的是,Spring Cloud 不支持Zuul 2 ,并且可能永远不会支持 。

现在,Spring Cloud Gateway是Spring Cloud Team首选的API网关实现。 它基于Spring 5,Reactor和Spring WebFlux构建。 不仅如此,它还包括断路器集成,使用Eureka进行服务发现,并且与OAuth 2.0集成起来容易得多

让我们深入。

创建一个Spring Cloud Eureka Server项目

首先创建一个目录来保存您的所有项目,例如spring-cloud-gateway 。 在终端窗口中导航至它,并创建一个包括Spring Cloud Eureka Server作为依赖项的discovery-service项目。

http https://start.spring.io/starter.zip javaVersion==11 artifactId==discovery-service \name==eureka-service baseDir==discovery-service \dependencies==cloud-eureka-server | tar -xzvf -

上面的命令使用HTTPie 。 我强烈建议安装它。 您也可以使用curl 。 运行curl https://start.spring.io以查看语法。

在其主类上添加@EnableEurekaServer ,以将其用作Eureka服务器。

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {...}

将以下属性添加到项目的src/main/resources/application.properties文件中,以配置其端口并关闭Eureka注册。

server.port=8761
eureka.client.register-with-eureka=false

要使discovery-service在Java 11+上运行,请添加对JAXB的依赖关系。

<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId>
</dependency>

使用./mvnw spring-boot:run或通过在IDE中运行它来启动项目。

创建一个Spring Cloud Gateway项目

接下来,创建一个包含一些Spring Cloud依赖项的api-gateway项目。

http https://start.spring.io/starter.zip javaVersion==11 artifactId==api-gateway \name==api-gateway baseDir==api-gateway \dependencies==actuator,cloud-eureka,cloud-feign,cloud-gateway,cloud-hystrix,webflux,lombok | tar -xzvf -

一分钟后,我们将重新配置该项目。

使用Spring WebFlux创建反应式微服务

汽车微服务将包含此示例代码的很大一部分,因为它包含支持CRUD(创建,读取,更新和删除)的功能齐全的REST API。

使用start.spring.io创建car-service项目:

http https://start.spring.io/starter.zip javaVersion==11 artifactId==car-service \name==car-service baseDir==car-service \dependencies==actuator,cloud-eureka,webflux,data-mongodb-reactive,flapdoodle-mongo,lombok | tar -xzvf -

这个命令中的dependencies参数很有趣。 您可以看到其中包括Spring WebFlux,以及MongoDB。 Spring Data还为Redis和Cassandra提供了响应式驱动程序。

您可能还对R2DBC (反应性关系数据库连接)感兴趣, R2DBC是一种将反应性编程API引入SQL数据库的工作。 在本示例中,我没有使用它,因为在start.spring.io上尚不可用。

使用Spring WebFlux构建REST API

我是大众的忠实拥护者,尤其是经典的公交车和bug车。 您是否知道大众在未来几年内将推出大量电动汽车? 我对ID Buzz感到非常兴奋! 它具有经典曲线,全电动。 它甚至拥有350匹以上的马力!

如果您不熟悉ID Buzz,请看这张来自大众汽车的照片。

让我们从这个API示例中获得一些乐趣,并将电动VW用于我们的数据集。 该API将跟踪各种汽车名称和发布日期。

src/main/java/…​/CarServiceApplication.java添加Eureka注册,示例数据初始化和响应式REST API:

package com.example.carservice;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import java.util.UUID;@EnableEurekaClient (1)
@SpringBootApplication
@Slf4j (2)
public class CarServiceApplication {public static void main(String[] args) {SpringApplication.run(CarServiceApplication.class, args);}@Bean (3)ApplicationRunner init(CarRepository repository) {// Electric VWs from https://www.vw.com/electric-concepts/// Release dates from https://www.motor1.com/features/346407/volkswagen-id-price-on-sale/Car ID = new Car(UUID.randomUUID(), "ID.", LocalDate.of(2019, Month.DECEMBER, 1));Car ID_CROZZ = new Car(UUID.randomUUID(), "ID. CROZZ", LocalDate.of(2021, Month.MAY, 1));Car ID_VIZZION = new Car(UUID.randomUUID(), "ID. VIZZION", LocalDate.of(2021, Month.DECEMBER, 1));Car ID_BUZZ = new Car(UUID.randomUUID(), "ID. BUZZ", LocalDate.of(2021, Month.DECEMBER, 1));Set<Car> vwConcepts = Set.of(ID, ID_BUZZ, ID_CROZZ, ID_VIZZION);return args -> {repository.deleteAll() (4).thenMany(Flux.just(vwConcepts).flatMap(repository::saveAll)).thenMany(repository.findAll()).subscribe(car -> log.info("saving " + car.toString())); (5)};}
}@Document
@Data
@NoArgsConstructor
@AllArgsConstructor
class Car { (6)@Idprivate UUID id;private String name;private LocalDate releaseDate;
}interface CarRepository extends ReactiveMongoRepository<Car, UUID> { 
} (7)@RestController
class CarController { (8)private CarRepository carRepository;public CarController(CarRepository carRepository) {this.carRepository = carRepository;}@PostMapping("/cars")@ResponseStatus(HttpStatus.CREATED)public Mono<Car> addCar(@RequestBody Car car) { (9)return carRepository.save(car);}@GetMapping("/cars")public Flux<Car> getCars() { (10)return carRepository.findAll();}@DeleteMapping("/cars/{id}")public Mono<ResponseEntity<Void>> deleteCar(@PathVariable("id") UUID id) {return carRepository.findById(id).flatMap(car -> carRepository.delete(car).then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))).defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));}
}
  1. 添加@EnableEurekaClient批注以进行服务发现
  2. @Slf4j是Lombok的便捷注释,可用于登录类
  3. ApplicationRunner bean用默认数据填充MongoDB
  4. 删除MongoDB中的所有现有数据,这样就不会添加新数据
  5. 订阅结果,以便调用deleteAll()saveAll()
  6. 带有Spring Data NoSQL和Lombok注释的Car类,以减少样板
  7. CarRepository接口扩展了ReactiveMongoRepository ,几乎没有任何代码,可为您提供CRUDability!
  8. 使用CarRepository执行CRUD操作的CarController
  9. Spring WebFlux返回单个对象的Mono发布者
  10. 返回多个对象的Flex发布者

如果使用IDE来构建项目,则需要为IDE设置Lombok 。

您还需要修改car-service项目的application.properties以设置其名称和端口。

spring.application.name=car-service
server.port=8081

运行MongoDB

运行MongoDB的最简单方法是从car-service/pom.xml的flappoodle依赖项中删除test范围。 这将导致您的应用程序启动嵌入式MongoDB依赖关系。

<dependency><groupId>de.flapdoodle.embed</groupId><artifactId>de.flapdoodle.embed.mongo</artifactId><!--<scope>test</scope>-->
</dependency>

您还可以使用Homebrew安装和运行MongoDB。

brew tap mongodb/brew
brew install mongodb-community@4.2
mongod

或者,使用Docker:

docker run -d -it -p 27017:27017 mongo

使用WebFlux传输数据

这就完成了使用Spring WebFlux构建REST API所需完成的所有工作。

“可是等等!” 你可能会说。 “我以为WebFlux就是关于流数据的?”

在此特定示例中,您仍然可以从/cars端点流式传输数据,但不能在浏览器中。

除了使用服务器发送事件或WebSocket之外,浏览器无法使用流。 但是,非浏览器客户端可以通过发送具有application/stream+json值的Accept报头来获取JSON流(感谢Rajeev Singh的技巧)。

可以通过启动浏览器并使用HTTPie发出请求测试此时一切正常。 但是,编写自动化测试要好得多!

使用WebTestClient测试您的WebFlux API

WebClient是Spring WebFlux的一部分,可用于发出响应请求,接收响应以及使用有效负载填充对象。 伴随类WebTestClient可用于测试WebFlux API。 它包含与WebClient相似的请求方法,以及检查响应正文,状态和标头的方法。

修改car-service项目中的src/test/java/…​/CarServiceApplicationTests.java类以包含以下代码。

package com.example.carservice;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;import java.time.LocalDate;
import java.time.Month;
import java.util.Collections;
import java.util.UUID;@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,properties = {"spring.cloud.discovery.enabled = false"})
public class CarServiceApplicationTests {@AutowiredCarRepository carRepository;@AutowiredWebTestClient webTestClient;@Testpublic void testAddCar() {Car buggy = new Car(UUID.randomUUID(), "ID. BUGGY", LocalDate.of(2022, Month.DECEMBER, 1));webTestClient.post().uri("/cars").contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaType.APPLICATION_JSON_UTF8).body(Mono.just(buggy), Car.class).exchange().expectStatus().isCreated().expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8).expectBody().jsonPath("$.id").isNotEmpty().jsonPath("$.name").isEqualTo("ID. BUGGY");}@Testpublic void testGetAllCars() {webTestClient.get().uri("/cars").accept(MediaType.APPLICATION_JSON_UTF8).exchange().expectStatus().isOk().expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8).expectBodyList(Car.class);}@Testpublic void testDeleteCar() {Car buzzCargo = carRepository.save(new Car(UUID.randomUUID(), "ID. BUZZ CARGO",LocalDate.of(2022, Month.DECEMBER, 2))).block();webTestClient.delete().uri("/cars/{id}", Collections.singletonMap("id", buzzCargo.getId())).exchange().expectStatus().isOk();}
}

为了证明它有效,请运行./mvnw test 。 测试通过后,请拍一下自己的背!

如果您使用的是Windows,请使用mvnw test

将Spring Cloud Gateway与反应式微服务一起使用

要在同一IDE窗口中编辑所有三个项目,我发现创建一个聚合器pom.xml很有用。 在项目的父目录中创建pom.xml文件,然后将下面的XML复制到其中。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.okta.developer</groupId><artifactId>reactive-parent</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><name>reactive-parent</name><modules><module>discovery-service</module><module>car-service</module><module>api-gateway</module></modules>
</project>

创建此文件后,您应该能够在IDE中将其作为项目打开,并可以轻松地在项目之间导航。

api-gateway项目中,将@EnableEurekaClient添加到主类以使其能够感知Eureka。

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@EnableEurekaClient
@SpringBootApplication
public class ApiGatewayApplication {...}

然后,修改src/main/resources/application.properties文件以配置应用程序名称。

spring.application.name=gateway

ApiGatewayApplication创建一个RouteLocator bean,以配置路由。 您可以使用YAML配置Spring Cloud Gateway,但我更喜欢Java。

package com.example.apigateway;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;@EnableEurekaClient
@SpringBootApplication
public class ApiGatewayApplication {public static void main(String[] args) {SpringApplication.run(ApiGatewayApplication.class, args);}@Beanpublic RouteLocator customRouteLocator(RouteLocatorBuilder builder) {return builder.routes().route("car-service", r -> r.path("/cars").uri("lb://car-service")).build();}
}

更改了这些代码之后,您应该能够启动所有三个Spring Boot应用程序并点击http://localhost:8080/cars

$ http :8080/cars
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
transfer-encoding: chunked[{"id": "ff48f617-6cba-477c-8e8f-2fc95be96416","name": "ID. CROZZ","releaseDate": "2021-05-01"},{"id": "dd6c3c32-724c-4511-a02c-3348b226160a","name": "ID. BUZZ","releaseDate": "2021-12-01"},{"id": "97cfc577-d66e-4a3c-bc40-e78c3aab7261","name": "ID.","releaseDate": "2019-12-01"},{"id": "477632c8-2206-4f72-b1a8-e982e6128ab4","name": "ID. VIZZION","releaseDate": "2021-12-01"}
]

添加REST API来检索您喜欢的汽车

创建一个/fave-cars终结点,以/fave-cars不是您最喜欢的汽车。

首先,添加一个负载平衡的WebClient.Builder bean。

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {return WebClient.builder();
}

然后在同一文件中的ApiGatewayApplication类下添加Car POJO和FaveCarsController

public class ApiGatewayApplication {...}
class Car {...}
class FaveCarsController {...}

使用WebClient检索汽车并过滤掉您不喜欢的汽车。

@Data
class Car {private String name;private LocalDate releaseDate;
}@RestController
class FaveCarsController {private final WebClient.Builder carClient;public FaveCarsController(WebClient.Builder carClient) {this.carClient = carClient;}@GetMapping("/fave-cars")public Flux<Car> faveCars() {return carClient.build().get().uri("lb://car-service/cars").retrieve().bodyToFlux(Car.class).filter(this::isFavorite);}private boolean isFavorite(Car car) {return car.getName().equals("ID. BUZZ");}
}

如果您没有使用自动为您导入的IDE,则需要将以下内容复制/粘贴到ApiGatewayApplication.java的顶部:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

重新启动网关应用程序以查看http://localhost:8080/fave-cars终结点仅返回ID Buzz。

Hystrix的故障转移呢?

在撰写本文时,Spring Cloud Gateway 仅支持Hystrix 。 Spring Cloud不赞成直接支持Hystrix,而是使用Spring Cloud Breaker 。 不幸的是,该库尚未发布GA版本,因此我决定不使用它。

要将Hystrix与Spring Cloud Gateway结合使用,可以向car-service路线添加过滤器,如下所示:

.route("car-service", r -> r.path("/cars").filters(f -> f.hystrix(c -> c.setName("carsFallback").setFallbackUri("forward:/cars-fallback"))).uri("lb://car-service/cars"))
.build();

然后创建一个CarsFallback控制器来处理/cars-fallback路由。

@RestController
class CarsFallback {@GetMapping("/cars-fallback")public Flux<Car> noCars() {return Flux.empty();}
}

首先,重新启动网关,并确认http://localhost:8080/cars可以正常工作。 然后关闭汽车服务,再试一次,您会看到它现在返回一个空数组。 重新启动汽车服务,您将再次看到该列表。

您已经使用Spring Cloud Gateway和Spring WebFlux构建了弹性和反应性的微服务架构。 现在,让我们看看如何保护它!

Feign与Spring Cloud Gateway怎么样?

如果您想在WebFlux应用程序中使用Feign,请参阅feign 反应项目。 在这个特定示例中,我不需要Feign。

具有OAuth 2.0的安全Spring Cloud Gateway

OAuth 2.0是用于委托访问API的授权框架。 OIDC(或OpenID Connect)是OAuth 2.0之上的薄层,可提供身份验证。 Spring Security对这两个框架都有出色的支持,Okta也是如此。

您可以通过构建自己的服务器或使用开源实现,在不使用云身份提供商的情况下使用OAuth 2.0和OIDC。 但是,难道您不愿只使用诸如Okta之类一直在线的东西吗?

如果您已经拥有Okta帐户,请参见下面的在Okta中创建Web应用程序 。 否则,我们创建了一个Maven插件,该插件配置了一个免费的Okta开发人员帐户+一个OIDC应用程序(不到一分钟!)。

要使用它,请运行: ./mvnw com.okta:okta-maven-plugin:setup./mvnw com.okta:okta-maven-plugin:setup创建一个帐户并配置您的Spring Boot应用程序以与Okta一起使用。

在Okta中创建Web应用程序

登录到您的1563开发者帐户(或者注册 ,如果你没有一个帐户)。

  1. 在“ 应用程序”页面上,选择添加应用程序
  2. 在“创建新应用程序”页面上,选择“ Web”
  3. 为您的应用提供一个令人难忘的名称,将http://localhost:8080/login/oauth2/code/okta为登录重定向URI,选择刷新令牌 (除了授权代码 ),然后点击完成

将发行者(位于API > 授权服务器下 ),客户端ID和客户端密钥复制到两个项目的application.properties中。

okta.oauth2.issuer=$issuer
okta.oauth2.client-id=$clientId
okta.oauth2.client-secret=$clientSecret

接下来,将Okta Spring Boot启动器和Spring Cloud Security添加到网关的pom.xml

<dependency><groupId>com.okta.spring</groupId><artifactId>okta-spring-boot-starter</artifactId><version>1.2.1</version>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-security</artifactId>
</dependency>

这就是添加Okta OIDC登录所需要做的一切! 重新启动您的Gateway应用,并在浏览器中导航到http://localhost:8080/fave-cars ,以将其重定向到Okta以进行用户授权。

使您的网关成为OAuth 2.0资源服务器

您可能不会在网关本身上为您的应用程序构建UI。 您可能会改用SPA或移动应用程序。 要将网关配置为充当资源服务器(查找带有承载令牌的Authorization标头),请在与主类相同的目录中添加新的SecurityConfiguration类。

package com.example.apigateway;import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {// @formatter:offhttp.authorizeExchange().anyExchange().authenticated().and().oauth2Login().and().oauth2ResourceServer().jwt();return http.build();// @formatter:on}
}

带有Spring Cloud Gateway的CORS

如果您在UI上使用SPA,则还需要配置CORS。 您可以通过向CorsWebFilter添加CorsWebFilter bean来实现。

@Bean
CorsWebFilter corsWebFilter() {CorsConfiguration corsConfig = new CorsConfiguration();corsConfig.setAllowedOrigins(List.of("*"));corsConfig.setMaxAge(3600L);corsConfig.addAllowedMethod("*");corsConfig.addAllowedHeader("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfig);return new CorsWebFilter(source);
}

确保您的进口商品与以下商品相符。

import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

Spring Cloud Gateway的文档介绍了如何使用YAML或WebFluxConfigurer配置CORS。 不幸的是,我无法任其工作。

使用WebTestClient和JWT测试网关

如果您在网关中配置了CORS,则可以测试它是否可以与WebTestClient一起使用。 用以下代码替换ApiGatewayApplicationTests的代码。

import java.util.Map;
import java.util.function.Consumer;import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,properties = {"spring.cloud.discovery.enabled = false"})
public class ApiGatewayApplicationTests {@AutowiredWebTestClient webTestClient;@MockBean (1)ReactiveJwtDecoder jwtDecoder;@Testpublic void testCorsConfiguration() {Jwt jwt = jwt(); (2)when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt)); (3)WebTestClient.ResponseSpec response = webTestClient.put().uri("/").headers(addJwt(jwt)) (4).header("Origin", "http://example.com").exchange();response.expectHeader().valueEquals("Access-Control-Allow-Origin", "*");}private Jwt jwt() {return new Jwt("token", null, null,Map.of("alg", "none"), Map.of("sub", "betsy"));}private Consumer<HttpHeaders> addJwt(Jwt jwt) {return headers -> headers.setBearerAuth(jwt.getTokenValue());}
}
  1. 模拟ReactiveJwtDecoder以便您设置期望值并在解码时返回模拟
  2. 创建一个新的JWT
  3. 解码后返回相同的JWT
  4. 将JWT添加到带有Bearer前缀的Authorization标头中

我喜欢WebTestClient如何让您如此轻松地设置安全标头!

您已将Spring Cloud Gateway配置为使用OIDC登录并充当OAuth 2.0资源服务器,但是car服务仍在端口8081上可用。 我们修复一下,以便只有网关可以与之对话。

微服务通信的安全网关

将Okta Spring Boot启动器添加到car-service/pom.xml

<dependency><groupId>com.okta.spring</groupId><artifactId>okta-spring-boot-starter</artifactId><version>1.2.1</version>
</dependency>

okta.*属性从网关的application.properties复制到汽车服务的属性。 然后创建一个SecurityConfiguration类,使该应用程序成为OAuth 2.0资源服务器。

package com.example.carservice;import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {// @formatter:offhttp.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt();Okta.configureResourceServer401ResponseBody(http);return http.build();// @formatter:on}
}

而已! 重新启动您的汽车服务应用程序,现在它已受到匿名入侵者的保护。

$ http :8081/cars
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain
...401 Unauthorized

使用WebTestClient和JWT测试您的微服务

启用安全性后,您在car-service项目中添加的测试将不再起作用。 修改CarServiceApplicationTests.java的代码,以将JWT访问令牌添加到每个请求。

package com.example.carservice;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;import java.time.LocalDate;
import java.time.Month;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,properties = {"spring.cloud.discovery.enabled = false"})
public class CarServiceApplicationTests {@AutowiredCarRepository carRepository;@AutowiredWebTestClient webTestClient;@MockBeanReactiveJwtDecoder jwtDecoder;@Testpublic void testAddCar() {Car buggy = new Car(UUID.randomUUID(), "ID. BUGGY", LocalDate.of(2022, Month.DECEMBER, 1));Jwt jwt = jwt();when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));webTestClient.post().uri("/cars").contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaType.APPLICATION_JSON_UTF8).headers(addJwt(jwt)).body(Mono.just(buggy), Car.class).exchange().expectStatus().isCreated().expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8).expectBody().jsonPath("$.id").isNotEmpty().jsonPath("$.name").isEqualTo("ID. BUGGY");}@Testpublic void testGetAllCars() {Jwt jwt = jwt();when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));webTestClient.get().uri("/cars").accept(MediaType.APPLICATION_JSON_UTF8).headers(addJwt(jwt)).exchange().expectStatus().isOk().expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8).expectBodyList(Car.class);}@Testpublic void testDeleteCar() {Car buzzCargo = carRepository.save(new Car(UUID.randomUUID(), "ID. BUZZ CARGO",LocalDate.of(2022, Month.DECEMBER, 2))).block();Jwt jwt = jwt();when(this.jwtDecoder.decode(anyString())).thenReturn(Mono.just(jwt));webTestClient.delete().uri("/cars/{id}", Map.of("id", buzzCargo.getId())).headers(addJwt(jwt)).exchange().expectStatus().isOk();}private Jwt jwt() {return new Jwt("token", null, null,Map.of("alg", "none"), Map.of("sub", "dave"));}private Consumer<HttpHeaders> addJwt(Jwt jwt) {return headers -> headers.setBearerAuth(jwt.getTokenValue());}
}

再次运行测试,一切都会通过!

Spring Security 5.2中的模拟JWT支持

感谢Josh Cummings在JWT和WebTestClient方面的帮助。 Josh预览了Spring Security 5.2中的模拟JWT支持。

this.webTestClient.mutateWith(jwt()).post(...)

Josh还提供了一个示例测试,展示了如何模拟JWT的主题,范围和声明 。 该代码基于Spring Security 5.2.0.M3中的新功能。

Spring Security领域中的OAuth 2.0和JWT支持前景光明! 😎

中继访问令牌:网关到微服务

您只需为网关与该受保护的服务进行一个小小的更改即可。 这非常简单,我❤️!

ApiGatewayApplication.java ,添加一个过滤器,该过滤器将应用Spring Cloud Security中的TokenRelayGatewayFilterFactory

import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory;@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder,TokenRelayGatewayFilterFactory filterFactory) {return builder.routes().route("car-service", r -> r.path("/cars").filters(f -> f.filter(filterFactory.apply())).uri("lb://car-service/cars")).build();
}

该中继工厂尚未自动刷新访问令牌 。

重新启动您的API网关,您应该能够查看http://localhost:8080/cars并使一切正常运行。

很可爱,你不觉得吗?

进一步了解Spring的Cloud Cloud Gateway和反应式微服务

我几乎没有涉及Spring Cloud Gateway的功能。 如果您正在构建响应式微服务,建议您看看它。

请参阅Spring Cloud Gateway项目页面以获取更多信息,包括文档。 我还发现这些教程很有用:

  • Spring Cloud Gateway入门 – 2019年6月18日
  • Spring Cloud Gateway教程 – 2019年5月30日

您可以在spring-cloud-gateway目录的@ oktadeveloper / java-microservices-examples中找到此示例的源代码。

git clone https://github.com/oktadeveloper/java-microservices-examples.git
cd java-microservices-examples/spring-cloud-gateway

要了解有关使用Java和Spring进行微服务和反应式编程的更多信息,请查看这些文章。

  • 带有Spring Boot和Spring Cloud的Java微服务
  • 带有Spring Cloud Config和JHipster的Java微服务
  • 通过Java,Docker和Spring Boot获得Jibby
  • 构建Spring微服务并对其进行Dockerize生产
  • 使用Spring WebFlux构建反应性API

如果您喜欢本教程, 请在Twitter上关注@oktadev 。 我们还会定期将截屏视频发布到我们的YouTube频道 。

使用Spring Cloud Gateway的安全反应微服务最初于2019年8月28日发布在Okta开发者博客上。

朋友不允许朋友写用户身份验证。 厌倦了管理自己的用户? 立即尝试Okta的API和Java SDK。 在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护。

翻译自: https://www.javacodegeeks.com/2019/10/secure-reactive-microservices-with-spring-cloud-gateway.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/340689.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pythonqt4上位机开发_「新阁教育」自由口通信上位机实战案例

1、引言组态软件作为一种通用软件&#xff0c;体系结构较为庞大、功能软件包多、价格也比较昂贵&#xff0c;而且对于一些复杂的业务逻辑或自定义的协议&#xff0c;实现起来比较麻烦。近几年&#xff0c;C#/.NET上位机开发应用越来越广泛&#xff0c;相对于传统的组态软件的方…

判断单链表中的元素是否递增_检测单链表中是否有环(C语言)

检测单链表中是否有环(C语言)方法&#xff1a;双指针法思路使用两个指针&#xff0c;两个初始时都指向链表的头结点&#xff0c;然后one指针一次加一&#xff0c;另一个two指针一次加二。在链表有环时&#xff0c;two指针与one指针相等就说明有环。当one指针到达环的起始位置时…

cad 怎么显示块句柄_cad怎么显示工具栏

大家使用电脑时可能会出现一些小问题&#xff0c;比如cad菜单栏不见了&#xff0c;怎样全屏显示等等问题&#xff0c;有时候不小心操作&#xff0c;导致问题的出现&#xff0c;cad怎么显示工具栏?cad怎么显示工具栏方法一、使用天正自带内部命令步骤1、在最下方的【命令栏】中…

Java 第16章 坦克大战1-2 思路整理

文章目录 1 绘制坦克坦克画板Panel画框Frame 2 让坦克动起来画板Panel 1 绘制坦克 坦克 不同坦克有共同属性&#xff0c;可以先提取共同特征&#xff08;坐标位置&#xff09;作为父类&#xff0c;然后其他坦克类继承它。 画板Panel 有坦克在画板上显示&#xff0c;所以包含…

java object转泛型_为什么Java的泛型要用擦除实现

在 Java 中的 泛型 &#xff0c;常常被称之为 伪泛型 &#xff0c;究其原因是因为在实际代码的运行中&#xff0c;将实际类型参数的信息擦除掉了 (Type Erasure) 。那是什么原因导致了 Java 做出这种妥协的呢&#xff1f;下面我就带着大家以 Java 语言设计者的角度&#xff0c;…

java lambda函数_Java SE 8新功能介绍:使用Lambda Expression进行函数式编程

java lambda函数“ Java SE 8新功能浏览 ”系列的这篇文章将深入了解Lambda表达式 。 我将向您展示Lambda表达式的几种不同用法。 它们都具有功能接口的共同实现。 我将解释编译器如何从代码中推断信息&#xff0c;例如特定类型的变量以及后台实际发生的情况。 在上一篇文章“…

Apache Camel 3.1 –更多骆驼核心优化(第2部分)

我以前曾在博客中介绍我们在下一个Camel 3.1版本&#xff08;第1部分&#xff09;中所做的优化 。 今天&#xff0c;我想发布大约4周后的最新状态更新。 我们集中在三个方面优化骆驼核心&#xff1a; 不必要的对象分配 不必要的方法调用 提高绩效 换句话说&#xff0c;我…

计算机系统的指令系统,计算机指令系统指的是什么呢?

2014-12-27计算机系统的指令由你哪两部分组成&#xff1f;共作用分别是什么&#xff1f;1。8086汇编语言指令由标号、操作码、操作数和注释组成,其中标号和注释可以省略&#xff0c;操作码指出指令要过盛的功能,操作数指出完成的对象。2。变量和标号的区别是变量由伪指令定义&a…

echarts怎么保存图片到剪切板上_在电脑上怎么批量给图片编号以及怎么自动记录记事本txt文档时间...

电脑日益成为我们日常办公不可或缺的工具&#xff0c;除了必要的软件使用之外&#xff0c;也有一些直到今天还不那么为人所熟知的小技巧。而小编今天就暂时为大家介绍两个颇为常用的小技巧~分别是如何将图片批量编号以及自动记录记事本时间。技巧一、图片批量编号旅行或者活动结…

计算机类qq网名,最帅的qq名字

qq这个聊天工具已经成为人们电脑里必备的软件&#xff0c;上到40~50的叔叔阿姨&#xff0c;下至8~9岁的小学生&#xff0c;都有qq这个聊天工具。qq是有可以随意改名字的功能&#xff0c;最长可以输入十几个字符作为名字。就是因为取名没有太多的限制&#xff0c;反而让人不知道…

c++中求解非线性方程组_齐次线性方程组的基础解系的简便算法

线性方程组的求解是线性代数中的基本技能&#xff0c;而齐次线性方程组的基础解系的求法又是基础。本文给出一个计算齐次线性方程组的基础解系的公式&#xff0c;从而简化计算过程。01 符号说明 n元线性方程组的矩阵形式&#xff1a;(1)齐次线性方程组;(2)非齐次线性方程组;系数…

python的if语句例句_Python入门之if条件语句

Besides the while statement just introduced, Python knows the usual control flow statements known from other languages, with some twists.除了之前介绍的while语句&#xff0c;Python同样支持其他语言通常用的控制流语句&#xff0c;但也有一些区别。 if Statements P…

esp32 怎么分配freertos 堆栈大小_深度解剖~ FreeRtos阅读笔记2 任务创建、内核链表初始化...

2.FREERTOS任务创建、内核链表初始化硬件环境&#xff1a;cortex m4FreeRTOS版本:v8.0.1今天开始阅读freertos&#xff0c;阅读同时做下笔记&#xff0c;等哪天碰到移植问题再翻出来看看。2.1 任务、链表结构体源码中使用tskTCB来存储一个任务的所有信息&#xff0c;xLIST存储内…

Sigma IDE现在支持Python无服务器Lambda函数!

想想无服务器&#xff0c;使用Pythonic –全部在您的浏览器中&#xff01; &#xff08;好吧&#xff0c;这则新闻已经过了几周了&#xff0c;但是仍然……&#xff09; 如果您沉迷于整个无服务器的“事物”中 &#xff0c;您可能已经注意到我们&#xff0c;一个在SLAppForge臭…

idle不是python自带的开发工具_Python的开发工具

通常情况下&#xff0c;为了提高开发效率&#xff0c;需要使用相应的开发工具。进行Python开发也可以使用开发工具。下面将详细介绍Python自带的IDLE 一使用自带的IDLE 在安装Python后&#xff0c;会自动安装一个IDLE。它是一个Python shell(可以在打开的IDLE窗口的标题栏上看到…

java se 导原码_Java SE 8新功能导览:Java开发世界中的重大变化

java se 导原码我很自豪&#xff0c;像其他专业团队成员一样&#xff0c;是采用OpenJDK的成员之一&#xff0c;但是从过去8个月就加入了&#xff0c;我们经历了Java SE 8 开发&#xff0c;编译&#xff0c;编码&#xff0c;讨论等各个阶段&#xff0c;直到将其付诸实践为止。 。…

linux将日期和日历信息追加到文件中_Linux任务调度

crontab 任务调度crontab 进行定时任务的设置概述 任务调度&#xff1a;是指系统在某个时间执行的特定的命令或程序。任务调度分类&#xff1a;1.系统工作&#xff1a;有些重要的工作必须周而复始地执行&#xff0c;如病毒扫描等 。2.个别用户工作&#xff1a;个别用户可能希望…

android sdk build-tools_从零开始仿写一个抖音App——视频编辑SDK开发(一)

本文首发于微信公众号——世界上有意思的事&#xff0c;搬运转载请注明出处&#xff0c;否则将追究版权责任。交流qq群&#xff1a;859640274。大家好久不见&#xff0c;又有一个多月没有发文章了。不知道还有哪些读者记得我的 从零开始仿写抖音App 的系列文章&#xff0c;这个…

爱默生E系列服务器机柜托盘,艾默生通信电源PS48300-3B/1800 一体化室内机柜

PS48300-3B/1800电源系统PS48300-3B/1800电源系统是艾默生网络能源集多年开发和网上运行经验&#xff0c;采用 DSP控制技术&#xff0c;为满足3G网络需求而设计的高可靠、高功率密度、高性能、全数 字化通信电源系统。根据交流配电和机柜高度。一、特点 1、休眠节能专利技术&am…

功能Java示例 第8部分–更多纯函数

这是第8部分&#xff0c;该系列的最后一部分称为“ Functional Java by Example”。 我在本系列的每个部分中发展的示例是某种“提要处理程序”&#xff0c;用于处理文档。 在上一期文章中&#xff0c;我们已经使用Vavr库看到了一些模式匹配&#xff0c;并且还将故障也视为数据…