From 0f875d89f3250135b8428e093c2670da616569bd Mon Sep 17 00:00:00 2001 From: Eric Zhao Date: Mon, 22 Apr 2019 16:27:40 +0800 Subject: [PATCH] Add Sentinel Spring Cloud Gateway adapter module and implementation Signed-off-by: Eric Zhao --- sentinel-adapter/pom.xml | 1 + .../README.md | 54 +++++ .../pom.xml | 82 +++++++ .../gateway/sc/SentinelGatewayFilter.java | 90 ++++++++ .../sc/ServerWebExchangeItemParser.java | 53 +++++ .../sc/api/GatewayApiMatcherManager.java | 65 ++++++ ...oudGatewayApiDefinitionChangeObserver.java | 33 +++ .../sc/api/matcher/WebExchangeApiMatcher.java | 70 ++++++ .../sc/callback/BlockRequestHandler.java | 38 ++++ .../callback/DefaultBlockRequestHandler.java | 93 ++++++++ .../sc/callback/GatewayCallbackManager.java | 68 ++++++ .../callback/RedirectBlockRequestHandler.java | 43 ++++ .../SentinelGatewayBlockExceptionHandler.java | 78 +++++++ .../gateway/sc/route/AntRoutePathMatcher.java | 55 +++++ .../sc/route/RegexRoutePathMatcher.java | 49 ++++ .../gateway/sc/route/RouteMatchers.java | 46 ++++ ...way.common.api.ApiDefinitionChangeObserver | 1 + .../gateway/sc/SentinelGatewayFilterTest.java | 91 ++++++++ .../sc/SpringCloudGatewayParamParserTest.java | 209 ++++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + 20 files changed, 1220 insertions(+) create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/README.md create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/GatewayApiMatcherManager.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/SpringCloudGatewayApiDefinitionChangeObserver.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/BlockRequestHandler.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/DefaultBlockRequestHandler.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/GatewayCallbackManager.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/RedirectBlockRequestHandler.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/exception/SentinelGatewayBlockExceptionHandler.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/AntRoutePathMatcher.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RegexRoutePathMatcher.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RouteMatchers.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SpringCloudGatewayParamParserTest.java create mode 100644 sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index 13e516cb..44531d92 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -23,6 +23,7 @@ sentinel-reactor-adapter sentinel-spring-webflux-adapter sentinel-api-gateway-adapter-common + sentinel-spring-cloud-gateway-adapter diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/README.md b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/README.md new file mode 100644 index 00000000..4961b32f --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/README.md @@ -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 + + com.alibaba.csp + sentinel-spring-cloud-gateway-adapter + x.y.z + +``` + +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 viewResolvers; + private final ServerCodecConfigurer serverCodecConfigurer; + + public GatewayConfiguration(ObjectProvider> 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`. \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml new file mode 100644 index 00000000..73c06db8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/pom.xml @@ -0,0 +1,82 @@ + + + + sentinel-adapter + com.alibaba.csp + 1.6.0-SNAPSHOT + + 4.0.0 + + sentinel-spring-cloud-gateway-adapter + jar + + + 1.8 + 1.8 + 2.1.1.RELEASE + 2.1.4.RELEASE + 5.1.5.RELEASE + + + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + + + com.alibaba.csp + sentinel-reactor-adapter + + + + org.springframework.cloud + spring-cloud-gateway-core + ${spring.cloud.gateway.version} + provided + + + org.springframework + spring-webflux + ${spring.version} + provided + + + + org.springframework.cloud + spring-cloud-starter-gateway + ${spring.cloud.gateway.version} + test + + + org.springframework.boot + spring-boot-starter-webflux + ${spring.boot.version} + test + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java new file mode 100644 index 00000000..dbe9c130 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilter.java @@ -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 paramParser = new GatewayParamParser<>( + new ServerWebExchangeItemParser()); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); + + Mono 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 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 pickMatchingApiDefinitions(ServerWebExchange exchange) { + return GatewayApiMatcherManager.getApiMatcherMap().values() + .stream() + .filter(m -> m.test(exchange)) + .map(WebExchangeApiMatcher::getApiName) + .collect(Collectors.toSet()); + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java new file mode 100644 index 00000000..6088cd2d --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/ServerWebExchangeItemParser.java @@ -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 { + + @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); + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/GatewayApiMatcherManager.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/GatewayApiMatcherManager.java new file mode 100644 index 00000000..5c1567ca --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/GatewayApiMatcherManager.java @@ -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 API_MATCHER_MAP = new ConcurrentHashMap<>(); + + public static Map getApiMatcherMap() { + return Collections.unmodifiableMap(API_MATCHER_MAP); + } + + public static Optional getMatcher(final String apiName) { + return Optional.ofNullable(apiName) + .map(e -> API_MATCHER_MAP.get(apiName)); + } + + public static Set getApiDefinitionSet() { + return API_MATCHER_MAP.values() + .stream() + .map(WebExchangeApiMatcher::getApiDefinition) + .collect(Collectors.toSet()); + } + + static synchronized void loadApiDefinitions(/*@Valid*/ Set 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() {} +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/SpringCloudGatewayApiDefinitionChangeObserver.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/SpringCloudGatewayApiDefinitionChangeObserver.java new file mode 100644 index 00000000..41781b7c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/SpringCloudGatewayApiDefinitionChangeObserver.java @@ -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 apiDefinitions) { + GatewayApiMatcherManager.loadApiDefinitions(apiDefinitions); + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java new file mode 100644 index 00000000..7abae52b --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/api/matcher/WebExchangeApiMatcher.java @@ -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 { + + 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> fromApiPredicate(/*@NonNull*/ ApiPredicateItem item) { + if (item instanceof ApiPathPredicateItem) { + return fromApiPathPredicate((ApiPathPredicateItem)item); + } + return Optional.empty(); + } + + private Optional> 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)); + } + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/BlockRequestHandler.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/BlockRequestHandler.java new file mode 100644 index 00000000..180ec19f --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/BlockRequestHandler.java @@ -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 handleRequest(ServerWebExchange exchange, Throwable t); +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/DefaultBlockRequestHandler.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/DefaultBlockRequestHandler.java new file mode 100644 index 00000000..32da88c8 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/DefaultBlockRequestHandler.java @@ -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 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 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 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; + } + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/GatewayCallbackManager.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/GatewayCallbackManager.java new file mode 100644 index 00000000..73c37d56 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/GatewayCallbackManager.java @@ -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 DEFAULT_ORIGIN_PARSER = (w) -> ""; + + /** + * BlockRequestHandler: (serverExchange, exception) -> response + */ + private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler(); + /** + * RequestOriginParser: (serverExchange) -> origin + */ + private static volatile Function 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 getRequestOriginParser() { + return requestOriginParser; + } + + public static void resetRequestOriginParser() { + GatewayCallbackManager.requestOriginParser = DEFAULT_ORIGIN_PARSER; + } + + public static void setRequestOriginParser(Function requestOriginParser) { + AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null"); + GatewayCallbackManager.requestOriginParser = requestOriginParser; + } + + private GatewayCallbackManager() {} +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/RedirectBlockRequestHandler.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/RedirectBlockRequestHandler.java new file mode 100644 index 00000000..cc942724 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/callback/RedirectBlockRequestHandler.java @@ -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 handleRequest(ServerWebExchange exchange, Throwable t) { + return ServerResponse.temporaryRedirect(uri).build(); + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/exception/SentinelGatewayBlockExceptionHandler.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/exception/SentinelGatewayBlockExceptionHandler.java new file mode 100644 index 00000000..73c139df --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/exception/SentinelGatewayBlockExceptionHandler.java @@ -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 viewResolvers; + private List> messageWriters; + + public SentinelGatewayBlockExceptionHandler(List viewResolvers, ServerCodecConfigurer serverCodecConfigurer) { + this.viewResolvers = viewResolvers; + this.messageWriters = serverCodecConfigurer.getWriters(); + } + + private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) { + return response.writeTo(exchange, contextSupplier.get()); + } + + @Override + public Mono 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 handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) { + return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable); + } + + private final Supplier contextSupplier = () -> new ServerResponse.Context() { + @Override + public List> messageWriters() { + return SentinelGatewayBlockExceptionHandler.this.messageWriters; + } + + @Override + public List viewResolvers() { + return SentinelGatewayBlockExceptionHandler.this.viewResolvers; + } + }; +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/AntRoutePathMatcher.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/AntRoutePathMatcher.java new file mode 100644 index 00000000..475373ea --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/AntRoutePathMatcher.java @@ -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 { + + 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; + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RegexRoutePathMatcher.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RegexRoutePathMatcher.java new file mode 100644 index 00000000..03355c8c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RegexRoutePathMatcher.java @@ -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 { + + 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; + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RouteMatchers.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RouteMatchers.java new file mode 100644 index 00000000..6f392144 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/gateway/sc/route/RouteMatchers.java @@ -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 all() { + return exchange -> true; + } + + public static Predicate antPath(String pathPattern) { + return new AntRoutePathMatcher(pathPattern); + } + + public static Predicate exactPath(final String path) { + return exchange -> exchange.getRequest().getPath().value().equals(path); + } + + public static Predicate regexPath(String pathPattern) { + return new RegexRoutePathMatcher(pathPattern); + } + + private RouteMatchers() {} +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver new file mode 100644 index 00000000..bcd8171c --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinitionChangeObserver @@ -0,0 +1 @@ +com.alibaba.csp.sentinel.adapter.gateway.sc.api.SpringCloudGatewayApiDefinitionChangeObserver \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java new file mode 100644 index 00000000..078bfec6 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SentinelGatewayFilterTest.java @@ -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 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() {{ + 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 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<>()); + } +} \ No newline at end of file diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SpringCloudGatewayParamParserTest.java b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SpringCloudGatewayParamParserTest.java new file mode 100644 index 00000000..5b9ca980 --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/gateway/sc/SpringCloudGatewayParamParserTest.java @@ -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 paramParser = new GatewayParamParser<>( + new ServerWebExchangeItemParser() + ); + + @Test + public void testParseParametersNoParamItem() { + // Mock a request. + ServerWebExchange exchange = mock(ServerWebExchange.class); + // Prepare gateway rules. + Set 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 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 expectedHeaders = new HashMap() {{ + 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 headerMap) { + HttpHeaders headers = mock(HttpHeaders.class); + when(request.getHeaders()).thenReturn(headers); + for (Map.Entry e : headerMap.entrySet()) { + when(headers.getFirst(e.getKey())).thenReturn(e.getValue()); + } + } + + @SuppressWarnings("unchecked") + private void mockUrlParams(/*@Mock*/ ServerHttpRequest request, Map paramMap) { + MultiValueMap urlParams = mock(MultiValueMap.class); + when(request.getQueryParams()).thenReturn(urlParams); + for (Map.Entry e : paramMap.entrySet()) { + when(urlParams.getFirst(e.getKey())).thenReturn(e.getValue()); + } + } + + @SuppressWarnings("unchecked") + private void mockSingleUrlParam(/*@Mock*/ ServerHttpRequest request, String key, String value) { + MultiValueMap 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<>()); + } +} diff --git a/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/sentinel-adapter/sentinel-spring-cloud-gateway-adapter/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file