使用Spring Cloud Gateway保護反應式微服務(二)

抽絲剝繭,細說架構那些事——【優銳課】html

接着上篇文章:使用Spring Cloud Gateway保護反應式微服務(一)java

咱們繼續~react

將Spring Cloud Gateway與反應式微服務一塊兒使用

要在同一個IDE窗口中編輯全部三個項目,我發現建立聚合器pom.xml頗有用。在項目的父目錄中建立pom.xml文件,而後將下面的XML複製到其中。git

 

<?xml version="1.0" encoding="UTF-8"?>github

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"web

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">spring

    <modelVersion>4.0.0</modelVersion>apache

    <groupId>com.okta.developer</groupId>json

    <artifactId>reactive-parent</artifactId>api

    <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,以配置路由。 你可使用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端點,以剔除你不喜歡的汽車。

首先,添加一個負載平衡的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-reactive項目。在這個特定示例中,我不須要Feign。

具備OAuth 2.0的安全Spring Cloud GatewaySecure

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建立一個賬戶並配置你的Spring Boot應用程序以與Okta一塊兒使用。

在Okta中建立Web應用程序

登陸你的Okta Developer賬戶(若是你沒有賬戶,請註冊)。

  1. 在「Applications」頁面上,選擇「Add Application」。
  2. 在「Create New Application」頁面上,選擇「 Web」。
  3. 給你的應用程序起一個使人難忘的名稱,將http://localhost:8080/login/oauth2/code/okta添加爲登陸重定向URI,選擇「Refresh Token」(除了「Authorization Code」),而後單擊「Done」。

將issuer(位於API > Authorization Servers下),客戶端ID和客戶端密鑰複製到兩個項目的application.properties中。

okta.oauth2.issuer=$issuer

okta.oauth2.client-id=$clientId

okta.oauth2.client-secret=$clientSecret

接下來,將Okta Spring Boot starter和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 header),請在與主類相同的目錄中添加新的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

若是你在用戶界面上使用SPA,則還須要配置CORS。你能夠經過向該類添加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<HttpHeaders> addJwt(Jwt jwt) {

        return headers -> headers.setBearerAuth(jwt.getTokenValue());

    }

}

  1. 模擬ReactiveJwtDecoder,以便你設置指望值並在解碼時返回模擬
  2. 建立一個新的JWT
  3. 解碼後返回相同的JWT
  4. 將JWT添加到帶有Bearer前綴的 Authorization header

我喜歡WebTestClient如何讓你如此輕鬆地設置security headers!你已將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 {

 

    @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<HttpHeaders> addJwt(Jwt jwt) {

        return headers -> headers.setBearerAuth(jwt.getTokenValue());

    }

}

再次運行測試,一切都會經過!

中繼訪問令牌:網關到微服務

你只需爲網關與該受保護的服務進行一個小小的更改便可。這很是簡單!

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並使一切正常運行。

很好,你不以爲嗎?

感謝閱讀!

另外近期整理了一套完整的java架構思惟導圖,分享給一樣正在認真學習的每位朋友~

相關文章
相關標籤/搜索