Add PrincipalNameKeyResolver
fixes gh-76
This commit is contained in:
@@ -67,6 +67,11 @@
|
||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security-reactive</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-eureka</artifactId>
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderWebFilt
|
||||
import org.springframework.cloud.gateway.filter.factory.SetStatusWebFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.WebFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
|
||||
import org.springframework.cloud.gateway.handler.FilteringWebHandler;
|
||||
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
|
||||
@@ -322,6 +323,12 @@ public class GatewayAutoConfiguration {
|
||||
return new RemoveResponseHeaderWebFilterFactory();
|
||||
}
|
||||
|
||||
@Bean(name = PrincipalNameKeyResolver.BEAN_NAME)
|
||||
@ConditionalOnBean(RateLimiter.class)
|
||||
public PrincipalNameKeyResolver principalNameKeyResolver() {
|
||||
return new PrincipalNameKeyResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({RateLimiter.class, KeyResolver.class})
|
||||
public RequestRateLimiterWebFilterFactory requestRateLimiterWebFilterFactory(RateLimiter rateLimiter) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter.Response;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -67,21 +68,25 @@ public class RequestRateLimiterWebFilterFactory implements WebFilterFactory, App
|
||||
// How much bursting do you want to allow?
|
||||
int capacity = args.getInt(BURST_CAPACITY_KEY);
|
||||
|
||||
String beanName = args.getString(KEY_RESOLVER_NAME_KEY);
|
||||
String beanName;
|
||||
if (args.hasFieldName(KEY_RESOLVER_NAME_KEY)) {
|
||||
beanName = args.getString(KEY_RESOLVER_NAME_KEY);
|
||||
} else {
|
||||
beanName = PrincipalNameKeyResolver.BEAN_NAME;
|
||||
}
|
||||
KeyResolver keyResolver = this.context.getBean(beanName, KeyResolver.class);
|
||||
|
||||
return (exchange, chain) ->
|
||||
keyResolver.resolve(exchange).flatMap(key -> {
|
||||
Response response = rateLimiter.isAllowed(key, replenishRate, capacity).block(); //FIXME: block()
|
||||
keyResolver.resolve(exchange).flatMap(key ->
|
||||
rateLimiter.isAllowed(key, replenishRate, capacity).flatMap(response -> {
|
||||
//TODO: set some headers for rate, tokens left
|
||||
|
||||
//TODO: set some headers for rate, tokens left
|
||||
|
||||
if (response.isAllowed()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
|
||||
return exchange.getResponse().setComplete();
|
||||
});
|
||||
if (response.isAllowed()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
|
||||
return exchange.getResponse().setComplete();
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import reactor.core.publisher.Mono;
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
//TODO: KeyResolver for exchange.getPrincipal().flatMap(principal -> {})
|
||||
public interface KeyResolver {
|
||||
Mono<String> resolve(ServerWebExchange exchange);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.springframework.cloud.gateway.filter.ratelimit;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
public class PrincipalNameKeyResolver implements KeyResolver {
|
||||
|
||||
public static final String BEAN_NAME = "principalNameKeyResolver";
|
||||
|
||||
@Override
|
||||
public Mono<String> resolve(ServerWebExchange exchange) {
|
||||
return exchange.getPrincipal().map(Principal::getName).switchIfEmpty(Mono.empty());
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.OrderedWebFilter;
|
||||
import org.springframework.cloud.gateway.filter.factory.WebFilterFactories;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
@@ -129,12 +130,20 @@ public class Routes {
|
||||
}
|
||||
|
||||
public WebFilterSpec webFilters(List<WebFilter> webFilters) {
|
||||
this.builder.webFilters(webFilters);
|
||||
this.addAll(webFilters);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebFilterSpec add(WebFilter webFilter) {
|
||||
this.builder.add(webFilter);
|
||||
return this.filter(webFilter);
|
||||
}
|
||||
|
||||
public WebFilterSpec filter(WebFilter webFilter) {
|
||||
return this.filter(webFilter, 0);
|
||||
}
|
||||
|
||||
public WebFilterSpec filter(WebFilter webFilter, int order) {
|
||||
this.builder.add(new OrderedWebFilter(webFilter, order));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.springframework.cloud.gateway.filter.ratelimit;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterWebFilterFactory;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.Routes;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.web.server.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapUserDetailsRepository;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RequestRateLimiterWebFilterFactory.BURST_CAPACITY_KEY;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RequestRateLimiterWebFilterFactory.REPLENISH_RATE_KEY;
|
||||
import static org.springframework.cloud.gateway.filter.factory.WebFilterFactories.prefixPath;
|
||||
import static org.springframework.cloud.gateway.handler.predicate.RoutePredicates.path;
|
||||
import static org.springframework.tuple.TupleBuilder.tuple;
|
||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = DEFINED_PORT)
|
||||
@ActiveProfiles("principalname")
|
||||
public class PrincipalNameKeyResolverIntegrationTests {
|
||||
@LocalServerPort
|
||||
protected int port = 0;
|
||||
|
||||
protected WebTestClient client;
|
||||
protected String baseUri;
|
||||
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.baseUri = "http://localhost:" + port;
|
||||
this.client = WebTestClient.bindToServer().baseUrl(baseUri).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyResolverWorks() {
|
||||
this.client.mutate()
|
||||
.filter(basicAuthentication("user", "password"))
|
||||
.build()
|
||||
.get()
|
||||
.uri("/myapi/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody().json("{\"user\":\"1\"}");
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/downstream")
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
protected static class TestConfig {
|
||||
|
||||
@Value("${server.port}")
|
||||
private int port;
|
||||
|
||||
@RequestMapping("/myapi/{id}")
|
||||
public Map<String, String> myapi(@PathVariable String id, Principal principal) {
|
||||
return Collections.singletonMap(principal.getName(), id);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouteLocator customRouteLocator(RequestRateLimiterWebFilterFactory rateLimiterFactory) {
|
||||
return Routes.locator()
|
||||
.route("protected-throttled")
|
||||
.uri("http://localhost:"+port)
|
||||
.predicate(path("/myapi/**"))
|
||||
.filter(rateLimiterFactory.apply(tuple().of(REPLENISH_RATE_KEY, 1, BURST_CAPACITY_KEY, 1)))
|
||||
.filter(prefixPath("/downstream"))
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
RateLimiter rateLimiter() {
|
||||
return (id, replenishRate, burstCapacity) -> Mono.just(new RateLimiter.Response(true, Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.httpBasic().and()
|
||||
.authorizeExchange()
|
||||
.pathMatchers("/myapi/**").authenticated()
|
||||
.anyExchange().permitAll()
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MapUserDetailsRepository userDetailsRepository() {
|
||||
UserDetails user = User.withUsername("user").password("password").roles("USER").build();
|
||||
return new MapUserDetailsRepository(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.security.config.web.server.HttpSecurity;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -181,6 +183,16 @@ public class BaseWebClientTests {
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.authorizeExchange()
|
||||
//.pathMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyExchange().permitAll()
|
||||
.and()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class TestRibbonConfig {
|
||||
|
||||
Reference in New Issue
Block a user