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