朋友不允许朋友写用户身份验证。 厌倦了管理自己的用户? 立即尝试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的依赖关系。
org.glassfish.jaxb
jaxb-runtime
使用./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 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)
@Id
private UUID id;
private String name;
private LocalDate releaseDate;
}
interface CarRepository extends ReactiveMongoRepository {
} (7)
@RestController
class CarController { (8)
private CarRepository carRepository;
public CarController(CarRepository carRepository) {
this.carRepository = carRepository;
}
@PostMapping("/cars")
@ResponseStatus(HttpStatus.CREATED)
public Mono addCar(@RequestBody Car car) { (9)
return carRepository.save(car);
}
@GetMapping("/cars")
public Flux getCars() { (10)
return carRepository.findAll();
}
@DeleteMapping("/cars/{id}")
public Mono> deleteCar(@PathVariable("id") UUID id) {
return carRepository.findById(id)
.flatMap(car -> carRepository.delete(car)
.then(Mono.just(new ResponseEntity(HttpStatus.OK)))
)
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}
- 添加
@EnableEurekaClient
批注以进行服务发现 -
@Slf4j
是Lombok的便捷注释,可用于登录类 -
ApplicationRunner
bean用默认数据填充MongoDB - 删除MongoDB中的所有现有数据,这样就不会添加新数据
- 订阅结果,以便调用
deleteAll()
和saveAll()
- 带有Spring Data NoSQL和Lombok注释的
Car
类,以减少样板 -
CarRepository
接口扩展了ReactiveMongoRepository
,几乎没有任何代码,可为您提供CRUDability! - 使用
CarRepository
执行CRUD操作的CarController
类 - Spring WebFlux返回单个对象的
Mono
发布者 - 返回多个对象的
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依赖关系。
de.flapdoodle.embed
de.flapdoodle.embed.mongo
您还可以使用Homebrew安装和运行MongoDB。
brew tap mongodb/brew
brew install [email protected]
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 {
@Autowired
CarRepository carRepository;
@Autowired
WebTestClient webTestClient;
@Test
public 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");
}
@Test
public void testGetAllCars() {
webTestClient.get().uri("/cars")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBodyList(Car.class);
}
@Test
public 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复制到其中。
4.0.0
com.okta.developer
reactive-parent
1.0.0-SNAPSHOT
pom
reactive-parent
discovery-service
car-service
api-gateway
创建此文件后,您应该能够在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);
}
@Bean
public 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 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 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开发者帐户(或者注册 ,如果你没有一个帐户)。
- 在“ 应用程序”页面上,选择添加应用程序 。
- 在“创建新应用程序”页面上,选择“ Web” 。
- 为您的应用提供一个令人难忘的名称,将
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
:
com.okta.spring
okta-spring-boot-starter
1.2.1
org.springframework.cloud
spring-cloud-security
这就是添加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 {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.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 {
@Autowired
WebTestClient webTestClient;
@MockBean (1)
ReactiveJwtDecoder jwtDecoder;
@Test
public 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 addJwt(Jwt jwt) {
return headers -> headers.setBearerAuth(jwt.getTokenValue());
}
}
- 模拟
ReactiveJwtDecoder
以便您设置期望值并在解码时返回模拟 - 创建一个新的JWT
- 解码后返回相同的JWT
- 将JWT添加到带有
Bearer
前缀的Authorization
标头中
我喜欢WebTestClient
如何让您如此轻松地设置安全标头!
您已将Spring Cloud Gateway配置为使用OIDC登录并充当OAuth 2.0资源服务器,但是car服务仍在端口8081
上可用。 我们修复一下,以便只有网关可以与之对话。
微服务通信的安全网关
将Okta Spring Boot启动器添加到car-service/pom.xml
:
com.okta.spring
okta-spring-boot-starter
1.2.1
将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 {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.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 {
@Autowired
CarRepository carRepository;
@Autowired
WebTestClient webTestClient;
@MockBean
ReactiveJwtDecoder jwtDecoder;
@Test
public 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");
}
@Test
public 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);
}
@Test
public 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 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