Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -23,6 +23,7 @@ | |||||
<module>sentinel-reactor-adapter</module> | <module>sentinel-reactor-adapter</module> | ||||
<module>sentinel-spring-webflux-adapter</module> | <module>sentinel-spring-webflux-adapter</module> | ||||
<module>sentinel-api-gateway-adapter-common</module> | <module>sentinel-api-gateway-adapter-common</module> | ||||
<module>sentinel-spring-cloud-gateway-adapter</module> | |||||
</modules> | </modules> | ||||
<dependencyManagement> | <dependencyManagement> | ||||
@@ -0,0 +1,54 @@ | |||||
# Sentinel Spring Cloud Gateway Adapter | |||||
> Note: this module requires Java 8 or later version. | |||||
Sentinel provides integration module with Spring Cloud Gateway. | |||||
The integration module is based on the Sentinel Reactor Adapter. | |||||
Add the following dependency in `pom.xml` (if you are using Maven): | |||||
```xml | |||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> | |||||
<version>x.y.z</version> | |||||
</dependency> | |||||
``` | |||||
Then you only need to inject the corresponding `SentinelGatewayFilter` and `SentinelGatewayBlockExceptionHandler` instance | |||||
in Spring configuration. For example: | |||||
```java | |||||
@Configuration | |||||
public class GatewayConfiguration { | |||||
private final List<ViewResolver> viewResolvers; | |||||
private final ServerCodecConfigurer serverCodecConfigurer; | |||||
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, | |||||
ServerCodecConfigurer serverCodecConfigurer) { | |||||
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); | |||||
this.serverCodecConfigurer = serverCodecConfigurer; | |||||
} | |||||
@Bean | |||||
@Order(-1) | |||||
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { | |||||
// Register the block exception handler for Spring Cloud Gateway. | |||||
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); | |||||
} | |||||
@Bean | |||||
@Order(-1) | |||||
public GlobalFilter sentinelGatewayFilter() { | |||||
return new SentinelGatewayFilter(); | |||||
} | |||||
} | |||||
``` | |||||
The gateway adapter will regard all `routeId` (defined in Spring properties) and all customized API definitions | |||||
(defined in `GatewayApiDefinitionManager` of `sentinel-api-gateway-adapter-common` module) as resources. | |||||
You can register various customized callback in `GatewayCallbackManager`: | |||||
- `setBlockHandler`: register a customized `BlockRequestHandler` to handle the blocked request. The default implementation is `DefaultBlockRequestHandler`, which returns default message like `Blocked by Sentinel: FlowException`. |
@@ -0,0 +1,82 @@ | |||||
<?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"> | |||||
<parent> | |||||
<artifactId>sentinel-adapter</artifactId> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<version>1.6.0-SNAPSHOT</version> | |||||
</parent> | |||||
<modelVersion>4.0.0</modelVersion> | |||||
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> | |||||
<packaging>jar</packaging> | |||||
<properties> | |||||
<java.source.version>1.8</java.source.version> | |||||
<java.target.version>1.8</java.target.version> | |||||
<spring.cloud.gateway.version>2.1.1.RELEASE</spring.cloud.gateway.version> | |||||
<spring.boot.version>2.1.4.RELEASE</spring.boot.version> | |||||
<spring.version>5.1.5.RELEASE</spring.version> | |||||
</properties> | |||||
<dependencies> | |||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-api-gateway-adapter-common</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.alibaba.csp</groupId> | |||||
<artifactId>sentinel-reactor-adapter</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.cloud</groupId> | |||||
<artifactId>spring-cloud-gateway-core</artifactId> | |||||
<version>${spring.cloud.gateway.version}</version> | |||||
<scope>provided</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework</groupId> | |||||
<artifactId>spring-webflux</artifactId> | |||||
<version>${spring.version}</version> | |||||
<scope>provided</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.cloud</groupId> | |||||
<artifactId>spring-cloud-starter-gateway</artifactId> | |||||
<version>${spring.cloud.gateway.version}</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-webflux</artifactId> | |||||
<version>${spring.boot.version}</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.springframework.boot</groupId> | |||||
<artifactId>spring-boot-starter-test</artifactId> | |||||
<version>${spring.boot.version}</version> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>junit</groupId> | |||||
<artifactId>junit</artifactId> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.assertj</groupId> | |||||
<artifactId>assertj-core</artifactId> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.mockito</groupId> | |||||
<artifactId>mockito-core</artifactId> | |||||
<scope>test</scope> | |||||
</dependency> | |||||
</dependencies> | |||||
</project> |
@@ -0,0 +1,90 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||||
import java.util.Optional; | |||||
import java.util.Set; | |||||
import java.util.stream.Collectors; | |||||
import com.alibaba.csp.sentinel.EntryType; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; | |||||
import com.alibaba.csp.sentinel.adapter.reactor.ContextConfig; | |||||
import com.alibaba.csp.sentinel.adapter.reactor.EntryConfig; | |||||
import com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorTransformer; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.GatewayApiMatcherManager; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher.WebExchangeApiMatcher; | |||||
import org.springframework.cloud.gateway.filter.GatewayFilter; | |||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; | |||||
import org.springframework.cloud.gateway.filter.GlobalFilter; | |||||
import org.springframework.cloud.gateway.route.Route; | |||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import reactor.core.publisher.Mono; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter { | |||||
private final GatewayParamParser<ServerWebExchange> paramParser = new GatewayParamParser<>( | |||||
new ServerWebExchangeItemParser()); | |||||
@Override | |||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | |||||
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); | |||||
Mono<Void> asyncResult = chain.filter(exchange); | |||||
if (route != null) { | |||||
String routeId = route.getId(); | |||||
Object[] params = paramParser.parseParameterFor(routeId, exchange, | |||||
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID); | |||||
String origin = Optional.ofNullable(GatewayCallbackManager.getRequestOriginParser()) | |||||
.map(f -> f.apply(exchange)) | |||||
.orElse(""); | |||||
asyncResult = asyncResult.transform( | |||||
new SentinelReactorTransformer<>(new EntryConfig(routeId, EntryType.IN, | |||||
1, params, new ContextConfig(contextName(routeId), origin))) | |||||
); | |||||
} | |||||
Set<String> matchingApis = pickMatchingApiDefinitions(exchange); | |||||
for (String apiName : matchingApis) { | |||||
Object[] params = paramParser.parseParameterFor(apiName, exchange, | |||||
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME); | |||||
asyncResult = asyncResult.transform( | |||||
new SentinelReactorTransformer<>(new EntryConfig(apiName, EntryType.IN, 1, params)) | |||||
); | |||||
} | |||||
return asyncResult; | |||||
} | |||||
private String contextName(String route) { | |||||
return SentinelGatewayConstants.GATEWAY_CONTEXT_ROUTE_PREFIX + route; | |||||
} | |||||
Set<String> pickMatchingApiDefinitions(ServerWebExchange exchange) { | |||||
return GatewayApiMatcherManager.getApiMatcherMap().values() | |||||
.stream() | |||||
.filter(m -> m.test(exchange)) | |||||
.map(WebExchangeApiMatcher::getApiName) | |||||
.collect(Collectors.toSet()); | |||||
} | |||||
} |
@@ -0,0 +1,53 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||||
import java.net.InetSocketAddress; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.RequestItemParser; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class ServerWebExchangeItemParser implements RequestItemParser<ServerWebExchange> { | |||||
@Override | |||||
public String getPath(ServerWebExchange exchange) { | |||||
return exchange.getRequest().getPath().value(); | |||||
} | |||||
@Override | |||||
public String getRemoteAddress(ServerWebExchange exchange) { | |||||
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); | |||||
if (remoteAddress == null) { | |||||
return null; | |||||
} | |||||
return remoteAddress.getAddress().getHostAddress(); | |||||
} | |||||
@Override | |||||
public String getHeader(ServerWebExchange exchange, String key) { | |||||
return exchange.getRequest().getHeaders().getFirst(key); | |||||
} | |||||
@Override | |||||
public String getUrlParam(ServerWebExchange exchange, String paramName) { | |||||
return exchange.getRequest().getQueryParams().getFirst(paramName); | |||||
} | |||||
} |
@@ -0,0 +1,65 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api; | |||||
import java.util.Collections; | |||||
import java.util.Map; | |||||
import java.util.Optional; | |||||
import java.util.Set; | |||||
import java.util.concurrent.ConcurrentHashMap; | |||||
import java.util.stream.Collectors; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher.WebExchangeApiMatcher; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public final class GatewayApiMatcherManager { | |||||
private static final Map<String, WebExchangeApiMatcher> API_MATCHER_MAP = new ConcurrentHashMap<>(); | |||||
public static Map<String, WebExchangeApiMatcher> getApiMatcherMap() { | |||||
return Collections.unmodifiableMap(API_MATCHER_MAP); | |||||
} | |||||
public static Optional<WebExchangeApiMatcher> getMatcher(final String apiName) { | |||||
return Optional.ofNullable(apiName) | |||||
.map(e -> API_MATCHER_MAP.get(apiName)); | |||||
} | |||||
public static Set<ApiDefinition> getApiDefinitionSet() { | |||||
return API_MATCHER_MAP.values() | |||||
.stream() | |||||
.map(WebExchangeApiMatcher::getApiDefinition) | |||||
.collect(Collectors.toSet()); | |||||
} | |||||
static synchronized void loadApiDefinitions(/*@Valid*/ Set<ApiDefinition> definitions) { | |||||
if (definitions == null || definitions.isEmpty()) { | |||||
API_MATCHER_MAP.clear(); | |||||
return; | |||||
} | |||||
definitions.forEach(GatewayApiMatcherManager::addApiDefinition); | |||||
} | |||||
static void addApiDefinition(ApiDefinition definition) { | |||||
API_MATCHER_MAP.put(definition.getApiName(), new WebExchangeApiMatcher(definition)); | |||||
} | |||||
private GatewayApiMatcherManager() {} | |||||
} |
@@ -0,0 +1,33 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api; | |||||
import java.util.Set; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class SpringCloudGatewayApiDefinitionChangeObserver implements ApiDefinitionChangeObserver { | |||||
@Override | |||||
public void onChange(Set<ApiDefinition> apiDefinitions) { | |||||
GatewayApiMatcherManager.loadApiDefinitions(apiDefinitions); | |||||
} | |||||
} |
@@ -0,0 +1,70 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.api.matcher; | |||||
import java.util.Optional; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.matcher.AbstractApiMatcher; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.route.RouteMatchers; | |||||
import com.alibaba.csp.sentinel.util.StringUtil; | |||||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class WebExchangeApiMatcher extends AbstractApiMatcher<ServerWebExchange> { | |||||
public WebExchangeApiMatcher(ApiDefinition apiDefinition) { | |||||
super(apiDefinition); | |||||
} | |||||
@Override | |||||
protected void initializeMatchers() { | |||||
if (apiDefinition.getPredicateItems() != null) { | |||||
apiDefinition.getPredicateItems().forEach(item -> | |||||
fromApiPredicate(item).ifPresent(matchers::add)); | |||||
} | |||||
} | |||||
private Optional<Predicate<ServerWebExchange>> fromApiPredicate(/*@NonNull*/ ApiPredicateItem item) { | |||||
if (item instanceof ApiPathPredicateItem) { | |||||
return fromApiPathPredicate((ApiPathPredicateItem)item); | |||||
} | |||||
return Optional.empty(); | |||||
} | |||||
private Optional<Predicate<ServerWebExchange>> fromApiPathPredicate(/*@Valid*/ ApiPathPredicateItem item) { | |||||
String pattern = item.getPattern(); | |||||
if (StringUtil.isBlank(pattern)) { | |||||
return Optional.empty(); | |||||
} | |||||
switch (item.getMatchStrategy()) { | |||||
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX: | |||||
return Optional.of(RouteMatchers.regexPath(pattern)); | |||||
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX: | |||||
return Optional.of(RouteMatchers.antPath(pattern)); | |||||
default: | |||||
return Optional.of(RouteMatchers.exactPath(pattern)); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,38 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||||
import org.springframework.web.reactive.function.server.ServerResponse; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import reactor.core.publisher.Mono; | |||||
/** | |||||
* Reactive handler for the blocked request. | |||||
* | |||||
* @author Eric Zhao | |||||
*/ | |||||
@FunctionalInterface | |||||
public interface BlockRequestHandler { | |||||
/** | |||||
* Handle the blocked request. | |||||
* | |||||
* @param exchange server exchange object | |||||
* @param t block exception | |||||
* @return server response to return | |||||
*/ | |||||
Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t); | |||||
} |
@@ -0,0 +1,93 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||||
import java.util.List; | |||||
import org.springframework.http.HttpStatus; | |||||
import org.springframework.http.InvalidMediaTypeException; | |||||
import org.springframework.http.MediaType; | |||||
import org.springframework.web.reactive.function.server.ServerResponse; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import reactor.core.publisher.Mono; | |||||
import static org.springframework.web.reactive.function.BodyInserters.fromObject; | |||||
/** | |||||
* The default implementation of {@link BlockRequestHandler}. | |||||
* Compatible with Spring WebFlux and Spring Cloud Gateway. | |||||
* | |||||
* @author Eric Zhao | |||||
*/ | |||||
public class DefaultBlockRequestHandler implements BlockRequestHandler { | |||||
private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Sentinel: "; | |||||
@Override | |||||
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable ex) { | |||||
if (acceptsHtml(exchange)) { | |||||
return htmlErrorResponse(ex); | |||||
} | |||||
// JSON result by default. | |||||
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) | |||||
.contentType(MediaType.APPLICATION_JSON_UTF8) | |||||
.body(fromObject(buildErrorResult(ex))); | |||||
} | |||||
private Mono<ServerResponse> htmlErrorResponse(Throwable ex) { | |||||
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) | |||||
.contentType(MediaType.TEXT_PLAIN) | |||||
.syncBody(DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); | |||||
} | |||||
private ErrorResult buildErrorResult(Throwable ex) { | |||||
return new ErrorResult(HttpStatus.TOO_MANY_REQUESTS.value(), | |||||
DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName()); | |||||
} | |||||
/** | |||||
* Reference from {@code DefaultErrorWebExceptionHandler} of Spring Boot. | |||||
*/ | |||||
private boolean acceptsHtml(ServerWebExchange exchange) { | |||||
try { | |||||
List<MediaType> acceptedMediaTypes = exchange.getRequest().getHeaders().getAccept(); | |||||
acceptedMediaTypes.remove(MediaType.ALL); | |||||
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes); | |||||
return acceptedMediaTypes.stream() | |||||
.anyMatch(MediaType.TEXT_HTML::isCompatibleWith); | |||||
} catch (InvalidMediaTypeException ex) { | |||||
return false; | |||||
} | |||||
} | |||||
private static class ErrorResult { | |||||
private final int code; | |||||
private final String message; | |||||
ErrorResult(int code, String message) { | |||||
this.code = code; | |||||
this.message = message; | |||||
} | |||||
public int getCode() { | |||||
return code; | |||||
} | |||||
public String getMessage() { | |||||
return message; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,68 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||||
import java.util.function.Function; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public final class GatewayCallbackManager { | |||||
private static final Function<ServerWebExchange, String> DEFAULT_ORIGIN_PARSER = (w) -> ""; | |||||
/** | |||||
* BlockRequestHandler: (serverExchange, exception) -> response | |||||
*/ | |||||
private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler(); | |||||
/** | |||||
* RequestOriginParser: (serverExchange) -> origin | |||||
*/ | |||||
private static volatile Function<ServerWebExchange, String> requestOriginParser = DEFAULT_ORIGIN_PARSER; | |||||
public static /*@NonNull*/ BlockRequestHandler getBlockHandler() { | |||||
return blockHandler; | |||||
} | |||||
public static void resetBlockHandler() { | |||||
GatewayCallbackManager.blockHandler = new DefaultBlockRequestHandler(); | |||||
} | |||||
public static void setBlockHandler(BlockRequestHandler blockHandler) { | |||||
AssertUtil.notNull(blockHandler, "blockHandler cannot be null"); | |||||
GatewayCallbackManager.blockHandler = blockHandler; | |||||
} | |||||
public static /*@NonNull*/ Function<ServerWebExchange, String> getRequestOriginParser() { | |||||
return requestOriginParser; | |||||
} | |||||
public static void resetRequestOriginParser() { | |||||
GatewayCallbackManager.requestOriginParser = DEFAULT_ORIGIN_PARSER; | |||||
} | |||||
public static void setRequestOriginParser(Function<ServerWebExchange, String> requestOriginParser) { | |||||
AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null"); | |||||
GatewayCallbackManager.requestOriginParser = requestOriginParser; | |||||
} | |||||
private GatewayCallbackManager() {} | |||||
} |
@@ -0,0 +1,43 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.callback; | |||||
import java.net.URI; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import org.springframework.web.reactive.function.server.ServerResponse; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import reactor.core.publisher.Mono; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class RedirectBlockRequestHandler implements BlockRequestHandler { | |||||
private final URI uri; | |||||
public RedirectBlockRequestHandler(String url) { | |||||
AssertUtil.assertNotBlank(url, "url cannot be blank"); | |||||
this.uri = URI.create(url); | |||||
} | |||||
@Override | |||||
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) { | |||||
return ServerResponse.temporaryRedirect(uri).build(); | |||||
} | |||||
} |
@@ -0,0 +1,78 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.exception; | |||||
import java.util.List; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; | |||||
import com.alibaba.csp.sentinel.slots.block.BlockException; | |||||
import com.alibaba.csp.sentinel.util.function.Supplier; | |||||
import org.springframework.http.codec.HttpMessageWriter; | |||||
import org.springframework.http.codec.ServerCodecConfigurer; | |||||
import org.springframework.web.reactive.function.server.ServerResponse; | |||||
import org.springframework.web.reactive.result.view.ViewResolver; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import org.springframework.web.server.WebExceptionHandler; | |||||
import reactor.core.publisher.Mono; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class SentinelGatewayBlockExceptionHandler implements WebExceptionHandler { | |||||
private List<ViewResolver> viewResolvers; | |||||
private List<HttpMessageWriter<?>> messageWriters; | |||||
public SentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { | |||||
this.viewResolvers = viewResolvers; | |||||
this.messageWriters = serverCodecConfigurer.getWriters(); | |||||
} | |||||
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) { | |||||
return response.writeTo(exchange, contextSupplier.get()); | |||||
} | |||||
@Override | |||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { | |||||
if (exchange.getResponse().isCommitted()) { | |||||
return Mono.error(ex); | |||||
} | |||||
// This exception handler only handles rejection by Sentinel. | |||||
if (!BlockException.isBlockException(ex)) { | |||||
return Mono.error(ex); | |||||
} | |||||
return handleBlockedRequest(exchange, ex) | |||||
.flatMap(response -> writeResponse(response, exchange)); | |||||
} | |||||
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { | |||||
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); | |||||
} | |||||
private final Supplier<ServerResponse.Context> contextSupplier = () -> new ServerResponse.Context() { | |||||
@Override | |||||
public List<HttpMessageWriter<?>> messageWriters() { | |||||
return SentinelGatewayBlockExceptionHandler.this.messageWriters; | |||||
} | |||||
@Override | |||||
public List<ViewResolver> viewResolvers() { | |||||
return SentinelGatewayBlockExceptionHandler.this.viewResolvers; | |||||
} | |||||
}; | |||||
} |
@@ -0,0 +1,55 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||||
import org.springframework.util.AntPathMatcher; | |||||
import org.springframework.util.PathMatcher; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class AntRoutePathMatcher implements Predicate<ServerWebExchange> { | |||||
private final String pattern; | |||||
private final PathMatcher pathMatcher; | |||||
private final boolean canMatch; | |||||
public AntRoutePathMatcher(String pattern) { | |||||
AssertUtil.assertNotBlank(pattern, "pattern cannot be blank"); | |||||
this.pattern = pattern; | |||||
this.pathMatcher = new AntPathMatcher(); | |||||
this.canMatch = pathMatcher.isPattern(pattern); | |||||
} | |||||
@Override | |||||
public boolean test(ServerWebExchange exchange) { | |||||
String path = exchange.getRequest().getPath().value(); | |||||
if (canMatch) { | |||||
return pathMatcher.match(pattern, path); | |||||
} | |||||
return false; | |||||
} | |||||
public String getPattern() { | |||||
return pattern; | |||||
} | |||||
} |
@@ -0,0 +1,49 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||||
import java.util.regex.Pattern; | |||||
import com.alibaba.csp.sentinel.util.AssertUtil; | |||||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public class RegexRoutePathMatcher implements Predicate<ServerWebExchange> { | |||||
private final String pattern; | |||||
private final Pattern regex; | |||||
public RegexRoutePathMatcher(String pattern) { | |||||
AssertUtil.assertNotBlank(pattern, "pattern cannot be blank"); | |||||
this.pattern = pattern; | |||||
this.regex = Pattern.compile(pattern); | |||||
} | |||||
@Override | |||||
public boolean test(ServerWebExchange exchange) { | |||||
String path = exchange.getRequest().getPath().value(); | |||||
return regex.matcher(path).matches(); | |||||
} | |||||
public String getPattern() { | |||||
return pattern; | |||||
} | |||||
} |
@@ -0,0 +1,46 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc.route; | |||||
import com.alibaba.csp.sentinel.util.function.Predicate; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
/** | |||||
* @author Eric Zhao | |||||
* @since 1.6.0 | |||||
*/ | |||||
public final class RouteMatchers { | |||||
public static Predicate<ServerWebExchange> all() { | |||||
return exchange -> true; | |||||
} | |||||
public static Predicate<ServerWebExchange> antPath(String pathPattern) { | |||||
return new AntRoutePathMatcher(pathPattern); | |||||
} | |||||
public static Predicate<ServerWebExchange> exactPath(final String path) { | |||||
return exchange -> exchange.getRequest().getPath().value().equals(path); | |||||
} | |||||
public static Predicate<ServerWebExchange> regexPath(String pathPattern) { | |||||
return new RegexRoutePathMatcher(pathPattern); | |||||
} | |||||
private RouteMatchers() {} | |||||
} |
@@ -0,0 +1 @@ | |||||
com.alibaba.csp.sentinel.adapter.gateway.sc.api.SpringCloudGatewayApiDefinitionChangeObserver |
@@ -0,0 +1,91 @@ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||||
import java.util.Collections; | |||||
import java.util.HashSet; | |||||
import java.util.Set; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; | |||||
import org.junit.After; | |||||
import org.junit.Before; | |||||
import org.junit.Test; | |||||
import org.springframework.http.server.RequestPath; | |||||
import org.springframework.http.server.reactive.ServerHttpRequest; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
import static org.mockito.Mockito.mock; | |||||
import static org.mockito.Mockito.when; | |||||
/** | |||||
* Test cases for {@link SentinelGatewayFilter}. | |||||
* | |||||
* @author Eric Zhao | |||||
*/ | |||||
public class SentinelGatewayFilterTest { | |||||
@Test | |||||
public void testPickMatchingApiDefinitions() { | |||||
// Mock a request. | |||||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||||
ServerHttpRequest request = mock(ServerHttpRequest.class); | |||||
when(exchange.getRequest()).thenReturn(request); | |||||
RequestPath requestPath = mock(RequestPath.class); | |||||
when(request.getPath()).thenReturn(requestPath); | |||||
// Prepare API definitions. | |||||
Set<ApiDefinition> apiDefinitions = new HashSet<>(); | |||||
String apiName1 = "some_customized_api"; | |||||
ApiDefinition api1 = new ApiDefinition(apiName1) | |||||
.setPredicateItems(Collections.singleton( | |||||
new ApiPathPredicateItem().setPattern("/product/**") | |||||
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX) | |||||
)); | |||||
String apiName2 = "another_customized_api"; | |||||
ApiDefinition api2 = new ApiDefinition(apiName2) | |||||
.setPredicateItems(new HashSet<ApiPredicateItem>() {{ | |||||
add(new ApiPathPredicateItem().setPattern("/something")); | |||||
add(new ApiPathPredicateItem().setPattern("/other/**") | |||||
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX)); | |||||
}}); | |||||
apiDefinitions.add(api1); | |||||
apiDefinitions.add(api2); | |||||
GatewayApiDefinitionManager.loadApiDefinitions(apiDefinitions); | |||||
SentinelGatewayFilter filter = new SentinelGatewayFilter(); | |||||
when(requestPath.value()).thenReturn("/product/123"); | |||||
Set<String> matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||||
assertThat(matchingApis.size()).isEqualTo(1); | |||||
assertThat(matchingApis.contains(apiName1)).isTrue(); | |||||
when(requestPath.value()).thenReturn("/products"); | |||||
assertThat(filter.pickMatchingApiDefinitions(exchange).size()).isZero(); | |||||
when(requestPath.value()).thenReturn("/something"); | |||||
matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||||
assertThat(matchingApis.size()).isEqualTo(1); | |||||
assertThat(matchingApis.contains(apiName2)).isTrue(); | |||||
when(requestPath.value()).thenReturn("/other/foo/3"); | |||||
matchingApis = filter.pickMatchingApiDefinitions(exchange); | |||||
assertThat(matchingApis.size()).isEqualTo(1); | |||||
assertThat(matchingApis.contains(apiName2)).isTrue(); | |||||
} | |||||
@Before | |||||
public void setUp() { | |||||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||||
GatewayRuleManager.loadRules(new HashSet<>()); | |||||
} | |||||
@After | |||||
public void tearDown() { | |||||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||||
GatewayRuleManager.loadRules(new HashSet<>()); | |||||
} | |||||
} |
@@ -0,0 +1,209 @@ | |||||
/* | |||||
* Copyright 1999-2019 Alibaba Group Holding Ltd. | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||||
* you may not use this file except in compliance with the License. | |||||
* You may obtain a copy of the License at | |||||
* | |||||
* https://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
*/ | |||||
package com.alibaba.csp.sentinel.adapter.gateway.sc; | |||||
import java.net.InetAddress; | |||||
import java.net.InetSocketAddress; | |||||
import java.util.HashMap; | |||||
import java.util.HashSet; | |||||
import java.util.Map; | |||||
import java.util.Set; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.param.GatewayParamParser; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; | |||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; | |||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant; | |||||
import org.junit.After; | |||||
import org.junit.Before; | |||||
import org.junit.Test; | |||||
import org.springframework.http.HttpHeaders; | |||||
import org.springframework.http.server.RequestPath; | |||||
import org.springframework.http.server.reactive.ServerHttpRequest; | |||||
import org.springframework.util.MultiValueMap; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import static org.assertj.core.api.Assertions.assertThat; | |||||
import static org.mockito.Mockito.mock; | |||||
import static org.mockito.Mockito.when; | |||||
/** | |||||
* @author Eric Zhao | |||||
*/ | |||||
public class SpringCloudGatewayParamParserTest { | |||||
private final GatewayParamParser<ServerWebExchange> paramParser = new GatewayParamParser<>( | |||||
new ServerWebExchangeItemParser() | |||||
); | |||||
@Test | |||||
public void testParseParametersNoParamItem() { | |||||
// Mock a request. | |||||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||||
// Prepare gateway rules. | |||||
Set<GatewayFlowRule> rules = new HashSet<>(); | |||||
String routeId1 = "my_test_route_A"; | |||||
rules.add(new GatewayFlowRule(routeId1) | |||||
.setCount(5) | |||||
.setIntervalSec(1) | |||||
); | |||||
GatewayRuleManager.loadRules(rules); | |||||
Object[] params = paramParser.parseParameterFor(routeId1, exchange, | |||||
e -> e.getResourceMode() == 0); | |||||
assertThat(params.length).isZero(); | |||||
} | |||||
@Test | |||||
public void testParseParametersWithItems() { | |||||
// Mock a request. | |||||
ServerWebExchange exchange = mock(ServerWebExchange.class); | |||||
ServerHttpRequest request = mock(ServerHttpRequest.class); | |||||
when(exchange.getRequest()).thenReturn(request); | |||||
RequestPath requestPath = mock(RequestPath.class); | |||||
when(request.getPath()).thenReturn(requestPath); | |||||
// Prepare gateway rules. | |||||
Set<GatewayFlowRule> rules = new HashSet<>(); | |||||
String routeId1 = "my_test_route_A"; | |||||
String api1 = "my_test_route_B"; | |||||
String headerName = "X-Sentinel-Flag"; | |||||
String paramName = "p"; | |||||
GatewayFlowRule routeRule1 = new GatewayFlowRule(routeId1) | |||||
.setCount(2) | |||||
.setIntervalSec(2) | |||||
.setBurst(2) | |||||
.setParamItem(new GatewayParamFlowItem() | |||||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP) | |||||
); | |||||
GatewayFlowRule routeRule2 = new GatewayFlowRule(routeId1) | |||||
.setCount(10) | |||||
.setIntervalSec(1) | |||||
.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) | |||||
.setMaxQueueingTimeoutMs(600) | |||||
.setParamItem(new GatewayParamFlowItem() | |||||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) | |||||
.setFieldName(headerName) | |||||
); | |||||
GatewayFlowRule routeRule3 = new GatewayFlowRule(routeId1) | |||||
.setCount(20) | |||||
.setIntervalSec(1) | |||||
.setBurst(5) | |||||
.setParamItem(new GatewayParamFlowItem() | |||||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) | |||||
.setFieldName(paramName) | |||||
); | |||||
GatewayFlowRule routeRule4 = new GatewayFlowRule(routeId1) | |||||
.setCount(120) | |||||
.setIntervalSec(10) | |||||
.setBurst(30) | |||||
.setParamItem(new GatewayParamFlowItem() | |||||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST) | |||||
); | |||||
GatewayFlowRule apiRule1 = new GatewayFlowRule(api1) | |||||
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) | |||||
.setCount(5) | |||||
.setIntervalSec(1) | |||||
.setParamItem(new GatewayParamFlowItem() | |||||
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) | |||||
.setFieldName(paramName) | |||||
); | |||||
rules.add(routeRule1); | |||||
rules.add(routeRule2); | |||||
rules.add(routeRule3); | |||||
rules.add(routeRule4); | |||||
rules.add(apiRule1); | |||||
GatewayRuleManager.loadRules(rules); | |||||
String expectedHost = "hello.test.sentinel"; | |||||
String expectedAddress = "66.77.88.99"; | |||||
String expectedHeaderValue1 = "Sentinel"; | |||||
String expectedUrlParamValue1 = "17"; | |||||
mockClientHostAddress(request, expectedAddress); | |||||
Map<String, String> expectedHeaders = new HashMap<String, String>() {{ | |||||
put(headerName, expectedHeaderValue1); put("Host", expectedHost); | |||||
}}; | |||||
mockHeaders(request, expectedHeaders); | |||||
mockSingleUrlParam(request, paramName, expectedUrlParamValue1); | |||||
Object[] params = paramParser.parseParameterFor(routeId1, exchange, e -> e.getResourceMode() == 0); | |||||
assertThat(params.length).isEqualTo(4); | |||||
assertThat(params[routeRule1.getParamItem().getIndex()]).isEqualTo(expectedAddress); | |||||
assertThat(params[routeRule2.getParamItem().getIndex()]).isEqualTo(expectedHeaderValue1); | |||||
assertThat(params[routeRule3.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue1); | |||||
assertThat(params[routeRule4.getParamItem().getIndex()]).isEqualTo(expectedHost); | |||||
assertThat(paramParser.parseParameterFor(api1, exchange, e -> e.getResourceMode() == 0).length).isZero(); | |||||
String expectedUrlParamValue2 = "fs"; | |||||
mockSingleUrlParam(request, paramName, expectedUrlParamValue2); | |||||
params = paramParser.parseParameterFor(api1, exchange, e -> e.getResourceMode() == 1); | |||||
assertThat(params.length).isEqualTo(1); | |||||
assertThat(params[apiRule1.getParamItem().getIndex()]).isEqualTo(expectedUrlParamValue2); | |||||
} | |||||
private void mockClientHostAddress(/*@Mock*/ ServerHttpRequest request, String address) { | |||||
InetSocketAddress socketAddress = mock(InetSocketAddress.class); | |||||
when(request.getRemoteAddress()).thenReturn(socketAddress); | |||||
InetAddress inetAddress = mock(InetAddress.class); | |||||
when(inetAddress.getHostAddress()).thenReturn(address); | |||||
when(socketAddress.getAddress()).thenReturn(inetAddress); | |||||
} | |||||
private void mockHeaders(/*@Mock*/ ServerHttpRequest request, Map<String, String> headerMap) { | |||||
HttpHeaders headers = mock(HttpHeaders.class); | |||||
when(request.getHeaders()).thenReturn(headers); | |||||
for (Map.Entry<String, String> e : headerMap.entrySet()) { | |||||
when(headers.getFirst(e.getKey())).thenReturn(e.getValue()); | |||||
} | |||||
} | |||||
@SuppressWarnings("unchecked") | |||||
private void mockUrlParams(/*@Mock*/ ServerHttpRequest request, Map<String, String> paramMap) { | |||||
MultiValueMap<String, String> urlParams = mock(MultiValueMap.class); | |||||
when(request.getQueryParams()).thenReturn(urlParams); | |||||
for (Map.Entry<String, String> e : paramMap.entrySet()) { | |||||
when(urlParams.getFirst(e.getKey())).thenReturn(e.getValue()); | |||||
} | |||||
} | |||||
@SuppressWarnings("unchecked") | |||||
private void mockSingleUrlParam(/*@Mock*/ ServerHttpRequest request, String key, String value) { | |||||
MultiValueMap<String, String> urlParams = mock(MultiValueMap.class); | |||||
when(request.getQueryParams()).thenReturn(urlParams); | |||||
when(urlParams.getFirst(key)).thenReturn(value); | |||||
} | |||||
private void mockSingleHeader(/*@Mock*/ ServerHttpRequest request, String key, String value) { | |||||
HttpHeaders headers = mock(HttpHeaders.class); | |||||
when(request.getHeaders()).thenReturn(headers); | |||||
when(headers.getFirst(key)).thenReturn(value); | |||||
} | |||||
@Before | |||||
public void setUp() { | |||||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||||
GatewayRuleManager.loadRules(new HashSet<>()); | |||||
} | |||||
@After | |||||
public void tearDown() { | |||||
GatewayApiDefinitionManager.loadApiDefinitions(new HashSet<>()); | |||||
GatewayRuleManager.loadRules(new HashSet<>()); | |||||
} | |||||
} |
@@ -0,0 +1 @@ | |||||
mock-maker-inline |