在本文中,您將學習如何使用 Spring Cloud Gateway 為經過身份驗證的用戶啟用速率限制。為什么重要?API 網關是您的微服務系統的入口點。因此,您應該提供適當的安全級別。速率限制可以防止您的 API 遭受 DoS 攻擊並限制網絡抓取。
您可以使用 Spring Cloud Gateway 輕松配置速率限制。這個特性的基本介紹可以參考我的文章基於Redis做Spring Cloud Gateway 中的速率限制實踐-spring cloud 入門教程。同樣,今天我們也將使用 Redis 作為速率限制器的后端。此外,我們將配置 HTTP 基本身份驗證。當然,您可以提供一些更高級的身份驗證機制,例如 X509 證書或 OAuth2 登錄。如果您考慮一下,請閱讀我的文章Spring Cloud Gateway OAuth2 with Keycloak。
1. 依賴
讓我們從依賴開始。由於我們將創建一個集成測試,我們需要一些額外的庫。首先,我們將使用 Testcontainers 庫。它允許我們在 JUnit 測試期間運行 Docker 容器。我們將使用它來運行 Redis 和一個模擬服務器,它負責模擬下游服務。當然,我們需要包含一個帶有 Spring Cloud Gateway 和 Spring Data Redis 的 starter。要實現 HTTP 基本身份驗證,我們還需要包含 Spring Security。這是 Maven 中所需依賴項的完整列表pom.xml
。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mockserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java</artifactId>
<scope>test</scope>
</dependency>
2. 配置 HTTP 基本身份驗證
為了配置 HTTP 基本身份驗證,我們需要創建@Configuration
帶有@EnableWebFluxSecurity
. 這是因為 Spring Cloud Gateway 建立在 Spring WebFlux 和 Netty 之上。此外,我們將創建一組測試用戶MapReactiveUserDetailsService
。
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges ->
exchanges.anyExchange().authenticated())
.httpBasic();
http.csrf().disable();
return http.build();
}
@Bean
public MapReactiveUserDetailsService users() {
UserDetails user1 = User.builder()
.username("user1")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user2 = User.builder()
.username("user2")
.password("{noop}1234")
.roles("USER")
.build();
UserDetails user3 = User.builder()
.username("user3")
.password("{noop}1234")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user1, user2, user3);
}
}
3.配置Spring Cloud Gateway Rate Limiter key
需要使用名為 的組件啟用請求速率限制器功能GatewayFilter
。此過濾器采用可選 keyResolver
參數。該 KeyResolver
接口允許您創建可插拔策略,派生出限制請求的密鑰。在我們的例子中,它將是一個用戶登錄。一旦用戶成功通過身份驗證,其登錄信息就會存儲在 Spring 中SecurityContext
。為了檢索響應式應用程序的上下文,我們應該使用ReactiveSecurityContextHolder
.
@Bean
KeyResolver authUserKeyResolver() {
return exchange -> ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication()
.getPrincipal().toString());
}
4. 測試場景
在測試場景中,我們將模擬傳入流量。每個請求都需要有一個Authorization
包含用戶憑據的標頭。單個用戶每分鍾可以發送 4 個請求。超過該限制后,Spring Cloud Gateway 將返回 HTTP 代碼HTTP 429 - Too Many Requests
。流量被尋址到下游服務。因此,我們使用 Testcontainers 運行模擬服務器。
5. 測試 Spring Cloud Gateway 安全限速器
最后,我們可以進行測試實現。我將使用 JUnit4,因為我之前在示例存儲庫中的其他示例中使用過它。我們有三個用於速率限制器配置的參數:replenishRate
、burstCapacity
和requestedTokens
。由於我們還允許每秒少於 1 個請求,因此我們需要為burstCapacity
和設置正確的值requestedTokens
。簡而言之,該requestedTokens
屬性設置請求花費多少令牌。另一方面,burstCapacity
屬性是允許用戶的最大請求數(或成本)。
在測試過程中,我們在user1
、user2
和之間隨機設置用戶名user3
。測試重復 20 次。
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.DEFINED_PORT,
properties = {"rateLimiter.secure=true"})
@RunWith(SpringRunner.class)
public class GatewaySecureRateLimiterTest {
private static final Logger LOGGER =
LoggerFactory.getLogger(GatewaySecureRateLimiterTest.class);
private Random random = new Random();
@Rule
public TestRule benchmarkRun = new BenchmarkRule();
@ClassRule
public static MockServerContainer mockServer =
new MockServerContainer();
@ClassRule
public static GenericContainer redis =
new GenericContainer("redis:5.0.6").withExposedPorts(6379);
@Autowired
TestRestTemplate template;
@BeforeClass
public static void init() {
System.setProperty("spring.cloud.gateway.routes[0].id", "account-service");
System.setProperty("spring.cloud.gateway.routes[0].uri", "http://" + mockServer.getHost() + ":" + mockServer.getServerPort());
System.setProperty("spring.cloud.gateway.routes[0].predicates[0]", "Path=/account/**");
System.setProperty("spring.cloud.gateway.routes[0].filters[0]", "RewritePath=/account/(?<path>.*), /$\\{path}");
System.setProperty("spring.cloud.gateway.routes[0].filters[1].name", "RequestRateLimiter");
System.setProperty("spring.cloud.gateway.routes[0].filters[1].args.redis-rate-limiter.replenishRate", "1");
System.setProperty("spring.cloud.gateway.routes[0].filters[1].args.redis-rate-limiter.burstCapacity", "60");
System.setProperty("spring.cloud.gateway.routes[0].filters[1].args.redis-rate-limiter.requestedTokens", "15");
System.setProperty("spring.redis.host", redis.getHost());
System.setProperty("spring.redis.port", "" + redis.getMappedPort(6379));
new MockServerClient(mockServer.getContainerIpAddress(), mockServer.getServerPort())
.when(HttpRequest.request()
.withPath("/1"))
.respond(response()
.withBody("{\"id\":1,\"number\":\"1234567890\"}")
.withHeader("Content-Type", "application/json"));
}
@Test
@BenchmarkOptions(warmupRounds = 0, concurrency = 1, benchmarkRounds = 20)
public void testAccountService() {
String username = "user" + (random.nextInt(3) + 1);
HttpHeaders headers = createHttpHeaders(username,"1234");
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<Account> r = template
.exchange("/account/{id}", HttpMethod.GET, entity, Account.class, 1);
LOGGER.info("Received({}): status->{}, payload->{}, remaining->{}",
username, r.getStatusCodeValue(), r.getBody(), r.getHeaders().get("X-RateLimit-Remaining"));
}
private HttpHeaders createHttpHeaders(String user, String password) {
String notEncoded = user + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(notEncoded.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Basic " + encodedAuth);
return headers;
}
}
讓我們運行測試。感謝junit-benchmarks
庫,我們可以配置測試的輪數。每次我記錄來自網關的響應時,包括用戶名、HTTP 狀態、有效負載和X-RateLimit-Remaining
顯示剩余令牌數量的標頭。
結果如下。
使用 Zuul、Ribbon、Feign、Eureka 和 Sleuth、Zipkin 創建簡單spring cloud微服務用例-spring cloud 入門教程
微服務集成SPRING CLOUD SLEUTH、ELK 和 ZIPKIN 進行監控-spring cloud 入門教程
使用Hystrix 、Feign 和 Ribbon構建微服務-spring cloud 入門教程
使用 Spring Boot Admin 監控微服務-spring cloud 入門教程
基於Redis做Spring Cloud Gateway 中的速率限制實踐-spring cloud 入門教程
集成SWAGGER2服務-spring cloud 入門教程
Hystrix 簡介-spring cloud 入門教程
Hystrix 原理深入分析-spring cloud 入門教程
使用Apache Camel構建微服務-spring cloud 入門教程
集成 Kubernetes 來構建微服務-spring cloud 入門教程
集成SPRINGDOC OPENAPI 的微服務實踐-spring cloud 入門教程
SPRING CLOUD 微服務快速指南-spring cloud 入門教程
基於GraphQL的微服務實踐-spring cloud 入門教程
最火的Spring Cloud Gateway 為經過身份驗證的用戶啟用速率限制實踐-spring cloud 入門教程