javascript
spring react_使用Spring Cloud Gateway保护React式微服务
spring react
朋友不允許朋友寫用戶身份驗證。 厭倦了管理自己的用戶? 立即嘗試Okta的API和Java SDK。 數(shù)分鐘之內(nèi)即可在任何應(yīng)用程序中對用戶進行身份驗證,管理和保護。
所以你想完全React,是嗎? 大! React式編程是使您的應(yīng)用程序更高效的一種越來越流行的方式。 響應(yīng)式應(yīng)用程序異步調(diào)用響應(yīng),而不是調(diào)用資源并等待響應(yīng)。 這使他們可以釋放處理能力,僅在必要時執(zhí)行處理,并且比其他系統(tǒng)更有效地擴展。
Java生態(tài)系統(tǒng)在React框架中占有相當(dāng)大的份額,其中包括Play框架,Ratpack,Vert.x和Spring WebFlux。 像響應(yīng)式編程一樣,微服務(wù)架構(gòu)可以幫助大型團隊快速擴展,并可以使用上述任何出色的框架進行構(gòu)建。
今天,我想向您展示如何使用Spring Cloud Gateway,Spring Boot和Spring WebFlux構(gòu)建React性微服務(wù)架構(gòu)。 我們將利用Spring Cloud Gateway,因為API網(wǎng)關(guān)通常是云原生微服務(wù)體系結(jié)構(gòu)中的重要組件,為所有后端微服務(wù)提供了聚合層。
本教程向您展示如何使用REST API構(gòu)建微服務(wù),該API返回新車列表。 您將使用Eureka進行服務(wù)發(fā)現(xiàn),并使用Spring Cloud Gateway將請求路由到微服務(wù)。 然后,您將集成Spring Security,以便只有經(jīng)過身份驗證的用戶才能訪問您的API網(wǎng)關(guān)和微服務(wù)。
先決條件 : HTTPie (或cURL), Java 11+和Internet連接。
Spring Cloud Gateway與Zuul
Zuul是Netflix的API網(wǎng)關(guān)。 Zuul于2013年首次發(fā)布,最初并不具有React性,但Zuul 2是完全重寫后的版本,使其具有React性。 不幸的是,Spring Cloud 不支持Zuul 2 ,并且可能永遠(yuǎn)不支持 。
現(xiàn)在,Spring Cloud Gateway是Spring Cloud Team首選的API網(wǎng)關(guān)實現(xiàn)。 它基于Spring 5,Reactor和Spring WebFlux構(gòu)建。 不僅如此,它還包括斷路器集成,使用Eureka進行服務(wù)發(fā)現(xiàn),并且與OAuth 2.0集成起來要容易得多 !
讓我們深入。
創(chuàng)建一個Spring Cloud Eureka Server項目
首先創(chuàng)建一個目錄來保存所有項目,例如spring-cloud-gateway 。 在終端窗口中導(dǎo)航至它,并創(chuàng)建一個包含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服務(wù)器。
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer @SpringBootApplication public class EurekaServiceApplication {...}將以下屬性添加到項目的src/main/resources/application.properties文件中,以配置其端口并關(guān)閉Eureka注冊。
server.port=8761 eureka.client.register-with-eureka=false要使discovery-service在Java 11+上運行,請?zhí)砑訉AXB的依賴關(guān)系。
<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId> </dependency>使用./mvnw spring-boot:run或通過在IDE中運行它來啟動項目。
創(chuàng)建一個Spring Cloud Gateway項目
接下來,創(chuàng)建一個包含一些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創(chuàng)建React式微服務(wù)
汽車微服務(wù)將包含此示例代碼的很大一部分,因為它包含支持CRUD(創(chuàng)建,讀取,更新和刪除)的功能齊全的REST API。
使用start.spring.io創(chuàng)建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參數(shù)很有趣。 您可以看到其中包括Spring WebFlux,以及MongoDB。 Spring Data還為Redis和Cassandra提供了響應(yīng)式驅(qū)動程序。
您可能還對R2DBC (React性關(guān)系數(shù)據(jù)庫連接)感興趣, 這是將React性編程API引入SQL數(shù)據(jù)庫的一種嘗試。 在本示例中,我沒有使用它,因為在start.spring.io上尚不可用。
使用Spring WebFlux構(gòu)建REST API
我是大眾的忠實擁護者,尤其是經(jīng)典的公交車和bug車。 您是否知道大眾在未來幾年內(nèi)將推出大量電動汽車? 我對ID Buzz感到非常興奮! 它具有經(jīng)典曲線,全電動。 它甚至擁有350+的馬力!
如果您不熟悉ID Buzz,這是大眾汽車的照片。
讓我們從這個API示例中獲得一些樂趣,并將電動VW用于我們的數(shù)據(jù)集。 該API將跟蹤各種汽車名稱和發(fā)布日期。
在src/main/java/…?/CarServiceApplication.java添加Eureka注冊,示例數(shù)據(jù)初始化和響應(yīng)式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));} }如果使用IDE構(gòu)建項目,則需要為IDE設(shè)置Lombok 。
您還需要修改car-service項目的application.properties以設(shè)置其名稱和端口。
spring.application.name=car-service server.port=8081運行MongoDB
運行MongoDB的最簡單方法是從car-service/pom.xml的flappoodle依賴項中刪除test范圍。 這將導(dǎo)致您的應(yīng)用程序啟動嵌入式MongoDB依賴關(guān)系。
<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傳輸數(shù)據(jù)
這樣就完成了使用Spring WebFlux構(gòu)建REST API所需完成的所有工作。
“可是等等!” 你可能會說。 “我以為WebFlux就是關(guān)于流數(shù)據(jù)的?”
在此特定示例中,您仍然可以從/cars端點流式傳輸數(shù)據(jù),但不能在瀏覽器中。
除了使用服務(wù)器發(fā)送事件或WebSocket之外,瀏覽器無法使用流。 但是,非瀏覽器客戶端可以通過發(fā)送具有application/stream+json值的Accept報頭來獲取JSON流(感謝Rajeev Singh的提示)。
您可以通過啟動瀏覽器并使用HTTPie發(fā)出請求來測試此時一切正常。 但是,編寫自動化測試要好得多!
使用WebTestClient測試您的WebFlux API
WebClient是Spring WebFlux的一部分,可用于發(fā)出響應(yīng)請求,接收響應(yīng)以及使用有效負(fù)載填充對象。 伴隨類WebTestClient可用于測試WebFlux API。 它包含與WebClient相似的請求方法,以及檢查響應(yīng)正文,狀態(tài)和標(biāo)頭的方法。
修改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與響應(yīng)式微服務(wù)一起使用
要在同一IDE窗口中編輯所有三個項目,我發(fā)現(xiàn)創(chuàng)建一個聚合器pom.xml很有用。 在項目的父目錄中創(chuàng)建pom.xml文件,然后將下面的XML復(fù)制到其中。
<?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>創(chuàng)建此文件后,您應(yīng)該能夠在IDE中將其作為項目打開,并可以輕松地在項目之間導(dǎo)航。
在api-gateway項目中,將@EnableEurekaClient添加到主類以使其能夠感知Eureka。
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@EnableEurekaClient @SpringBootApplication public class ApiGatewayApplication {...}然后,修改src/main/resources/application.properties文件以配置應(yīng)用程序名稱。
spring.application.name=gateway在ApiGatewayApplication創(chuàng)建一個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();} }更改完這些代碼后,您應(yīng)該能夠啟動所有三個Spring Boot應(yīng)用程序并點擊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來檢索您喜歡的汽車
創(chuàng)建一個/fave-cars端點,以/fave-cars不是您最喜歡的汽車。
首先,添加一個負(fù)載平衡的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");} }如果您沒有使用自動為您導(dǎo)入的IDE,則需要將以下內(nèi)容復(fù)制/粘貼到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;重新啟動網(wǎng)關(guān)應(yīng)用程序以查看http://localhost:8080/fave-cars終結(jié)點僅返回ID Buzz。
Hystrix的故障轉(zhuǎn)移呢?
在撰寫本文時,Spring Cloud Gateway 僅支持Hystrix 。 Spring Cloud不贊成直接支持Hystrix,而是使用Spring Cloud Breaker 。 不幸的是,該庫尚未發(fā)布GA版本,因此我決定不使用它。
要將Hystrix與Spring Cloud Gateway結(jié)合使用,可以向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();然后創(chuàng)建一個CarsFallback控制器來處理/cars-fallback路由。
@RestController class CarsFallback {@GetMapping("/cars-fallback")public Flux<Car> noCars() {return Flux.empty();} }首先,重新啟動網(wǎng)關(guān),并確認(rèn)http://localhost:8080/cars可以正常工作。 然后關(guān)閉汽車服務(wù),再試一次,您會看到它現(xiàn)在返回一個空數(shù)組。 重新啟動汽車服務(wù),您將再次看到該列表。
您已經(jīng)使用Spring Cloud Gateway和Spring WebFlux構(gòu)建了彈性和React性的微服務(wù)架構(gòu)。 現(xiàn)在讓我們看看如何保護它!
Feign with Spring Cloud Gateway怎么樣?
如果您想在WebFlux應(yīng)用程序中使用Feign,請參閱feign - active項目。 在這個特定示例中,我不需要Feign。
帶有OAuth 2.0的安全Spring Cloud Gateway
OAuth 2.0是用于委托訪問API的授權(quán)框架。 OIDC(或OpenID Connect)是OAuth 2.0之上的薄層,可提供身份驗證。 Spring Security對這兩個框架都有出色的支持,Okta也是如此。
您可以通過構(gòu)建自己的服務(wù)器或使用開源實現(xiàn)來在沒有云身份提供商的情況下使用OAuth 2.0和OIDC。 但是,難道您不希望只使用諸如Okta之類一直在線的東西嗎?
如果您已經(jīng)有Okta帳戶,請參見下面的在Okta中創(chuàng)建Web應(yīng)用程序 。 否則,我們創(chuàng)建了一個Maven插件,該插件配置了一個免費的Okta開發(fā)者帳戶+一個OIDC應(yīng)用程序(不到一分鐘!)。
要使用它,請運行: ./mvnw com.okta:okta-maven-plugin:setup : ./mvnw com.okta:okta-maven-plugin:setup創(chuàng)建一個帳戶并配置您的Spring Boot應(yīng)用程序以與Okta一起使用。
在Okta中創(chuàng)建Web應(yīng)用程序
登錄到您的1563開發(fā)者帳戶(或者注冊 ,如果你沒有一個帳戶)。
將發(fā)行者(位于API > 授權(quán)服務(wù)器下 ),客戶端ID和客戶端密鑰復(fù)制到兩個項目的application.properties中。
okta.oauth2.issuer=$issuer okta.oauth2.client-id=$clientId okta.oauth2.client-secret=$clientSecret接下來,將Okta Spring Boot啟動器和Spring Cloud Security添加到網(wǎng)關(guān)的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應(yīng)用,并在瀏覽器中導(dǎo)航到http://localhost:8080/fave-cars ,以將其重定向到Okta以進行用戶授權(quán)。
使您的網(wǎng)關(guān)成為OAuth 2.0資源服務(wù)器
您可能不會在網(wǎng)關(guān)本身上為您的應(yīng)用程序構(gòu)建UI。 您可能會改用SPA或移動應(yīng)用程序。 要將網(wǎng)關(guān)配置為充當(dāng)資源服務(wù)器(查找?guī)в谐休d令牌的Authorization標(biāo)頭),請在與主類相同的目錄中添加一個新的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
如果您將SPA用于UI,則還需要配置CORS。 您可以通過向CorsWebFilter添加CorsWebFilter bean來實現(xiàn)。
@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測試網(wǎng)關(guān)
如果您在網(wǎng)關(guān)中配置了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());} }我喜歡WebTestClient如何讓您如此輕松地設(shè)置安全標(biāo)頭!
您已將Spring Cloud Gateway配置為使用OIDC登錄并充當(dāng)OAuth 2.0資源服務(wù)器,但是car服務(wù)仍在端口8081上可用。 我們修復(fù)一下,以便只有網(wǎng)關(guān)可以與它進行對話。
微服務(wù)通信的安全網(wǎng)關(guān)
將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.*屬性從網(wǎng)關(guān)的application.properties復(fù)制到汽車服務(wù)的屬性。 然后創(chuàng)建一個SecurityConfiguration類,使該應(yīng)用程序成為OAuth 2.0資源服務(wù)器。
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} }而已! 重新啟動您的汽車服務(wù)應(yīng)用程序,現(xiàn)在它已受到匿名入侵者的保護。
$ 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測試您的微服務(wù)
啟用安全性后,您在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預(yù)覽了Spring Security 5.2中的模擬JWT支持。
this.webTestClient.mutateWith(jwt()).post(...)Josh還提供了一個示例測試,展示了如何模擬JWT的主題,范圍和聲明 。 該代碼基于Spring Security 5.2.0.M3中的新功能。
Spring Security領(lǐng)域中的OAuth 2.0和JWT支持前景光明! 😎
中繼訪問令牌:網(wǎng)關(guān)到微服務(wù)
您只需為網(wǎng)關(guān)與該受保護的服務(wù)進行一個小小的更改即可。 這非常簡單,我??!
在ApiGatewayApplication.java ,添加一個過濾器,該過濾器將應(yīng)用來自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網(wǎng)關(guān),您應(yīng)該能夠查看http://localhost:8080/cars并使一切正常運行。
很可愛,你不覺得嗎?
了解有關(guān)Spring的更多信息Spring網(wǎng)關(guān)和響應(yīng)式微服務(wù)
我?guī)缀鯖]有涉及Spring Cloud Gateway的功能。 如果您要構(gòu)建響應(yīng)式微服務(wù),建議您看看它。
請參閱Spring Cloud Gateway項目頁面以獲取更多信息,包括文檔。 我還發(fā)現(xiàn)這些教程很有用:
- 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要了解有關(guān)使用Java和Spring進行微服務(wù)和React式編程的更多信息,請查看這些文章。
- 帶有Spring Boot和Spring Cloud的Java微服務(wù)
- 帶有Spring Cloud Config和JHipster的Java微服務(wù)
- 通過Java,Docker和Spring Boot獲得Jibby
- 構(gòu)建Spring微服務(wù)并對其進行Dockerize生產(chǎn)
- 使用Spring WebFlux構(gòu)建React性API
如果您喜歡本教程, 請在Twitter上關(guān)注@oktadev 。 我們還會定期將截屏視頻發(fā)布到我們的YouTube頻道 。
帶有Spring Cloud Gateway的安全React微服務(wù)最初于2019年8月28日發(fā)布在Okta開發(fā)者博客上。
朋友不允許朋友寫用戶身份驗證。 厭倦了管理自己的用戶? 立即嘗試Okta的API和Java SDK。 數(shù)分鐘之內(nèi)即可在任何應(yīng)用程序中對用戶進行身份驗證,管理和保護。
翻譯自: https://www.javacodegeeks.com/2019/10/secure-reactive-microservices-with-spring-cloud-gateway.html
spring react
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的spring react_使用Spring Cloud Gateway保护React式微服务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蓝牙耳机怎么选购
- 下一篇: 溯洄怎么读 溯洄的意思