Add PrincipalNameKeyResolver

fixes gh-76
This commit is contained in:
Spencer Gibb
2017-10-04 18:30:04 -04:00
parent b997943fb9
commit e1e78c8c4a
8 changed files with 198 additions and 14 deletions

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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();
}));
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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 {