diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index bb8d2bf75b..c4655eb085 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -16,6 +16,7 @@ sentinel-web-servlet + sentinel-web-servlet-6x sentinel-dubbo-adapter sentinel-apache-dubbo-adapter sentinel-apache-dubbo3-adapter diff --git a/sentinel-adapter/sentinel-web-servlet-6x/README.md b/sentinel-adapter/sentinel-web-servlet-6x/README.md new file mode 100755 index 0000000000..1d56d21b6e --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/README.md @@ -0,0 +1,66 @@ +# Sentinel Web Servlet Filter + +Sentinel provides Servlet filter integration to enable flow control for web requests. +Add the following dependency in `pom.xml` (if you are using Maven): + +```xml + + com.alibaba.csp + sentinel-web-servlet + x.y.z + +``` + +To activate the filter, you can simply configure your `web.xml` with: + +```xml + + SentinelCommonFilter + com.alibaba.csp.sentinel.adapter.servlet.CommonFilter + + + + SentinelCommonFilter + /* + +``` + +For Spring web applications you can configure with Spring bean: + +```java +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new CommonFilter()); + // Set the matching URL pattern for the filter. + registration.addUrlPatterns("/*"); + registration.setName("sentinelCommonFilter"); + registration.setOrder(1); + // Set whether to support the specified HTTP method prefix for the filter. + registration.addInitParameter(CommonFilter.HTTP_METHOD_SPECIFY, "false"); + return registration; + } +} +``` + +When a request is blocked, Sentinel servlet filter will display a default page indicating the request is rejected. +The HTTP status code of the default block page is **429 (Too Many Requests)**. You can customize it +via the `csp.sentinel.web.servlet.block.status` configuration item (since 1.7.0). + +If customized block page is set (via `WebServletConfig.setBlockPage(blockPage)` method), +the filter will redirect the request to provided URL. You can also implement your own +block handler (the `UrlBlockHandler` interface) and register to `WebCallbackManager`. + +The `UrlCleaner` interface is designed for clean and unify the URL resource. +For REST APIs, you have to clean the URL resource (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or +the amount of context and resources will exceed the threshold. + +If you need to exclude some URLs (that should not be recorded as Sentinel resources), you could also +leverage the `UrlCleaner` interface. You may unify the unwanted URLs to the empty string `""` or `null`, +then the URLs will be excluded (since Sentinel 1.6.3). + +The `RequestOriginParser` interface is useful for extracting request origin (e.g. IP or appName from HTTP Header) +from HTTP request. You can implement your own `RequestOriginParser` and register to `WebCallbackManager`. diff --git a/sentinel-adapter/sentinel-web-servlet-6x/pom.xml b/sentinel-adapter/sentinel-web-servlet-6x/pom.xml new file mode 100755 index 0000000000..d50509a496 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-adapter + 2.0.0-SNAPSHOT + + + sentinel-web-servlet-6x + jar + + + 6.0.0 + + + + + com.alibaba.csp + sentinel-core + + + + jakarta.servlet + jakarta.servlet-api + ${servlet.api.version} + + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-web + 3.0.0 + test + + + spring-boot-starter-logging + org.springframework.boot + + + + + org.springframework.boot + spring-boot-starter-test + 3.0.0 + test + + + \ No newline at end of file diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java new file mode 100755 index 0000000000..bedccb989d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java @@ -0,0 +1,145 @@ +/* + * Copyright 1999-2018 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.servlet; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Servlet filter that integrates with Sentinel. + * + * @author youji.zj + * @author Eric Zhao + * @author zhaoyuguang + * @author quguai.ly + */ +public class CommonFilter implements Filter { + + /** + * Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}). + */ + public static final String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY"; + /** + * If enabled, use the default context name, or else use the URL path as the context name, + * {@link WebServletConfig#WEB_SERVLET_CONTEXT_NAME}. Please pay attention to the number of context (EntranceNode), + * which may affect the memory footprint. + * + * @since 1.7.0 + */ + public static final String WEB_CONTEXT_UNIFY = "WEB_CONTEXT_UNIFY"; + + private final static String COLON = ":"; + + private boolean httpMethodSpecify = false; + private boolean webContextUnify = true; + + @Override + public void init(FilterConfig filterConfig) { + httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY)); + if (filterConfig.getInitParameter(WEB_CONTEXT_UNIFY) != null) { + webContextUnify = Boolean.parseBoolean(filterConfig.getInitParameter(WEB_CONTEXT_UNIFY)); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest) request; + Entry urlEntry = null; + + try { + String target = FilterUtil.filterTarget(sRequest); + // Clean and unify the URL. + // For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or + // the amount of context and resources will exceed the threshold. + UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner(); + if (urlCleaner != null) { + target = urlCleaner.clean(target); + } + + // If you intend to exclude some URLs, you can convert the URLs to the empty string "" + // in the UrlCleaner implementation. + if (!StringUtil.isEmpty(target)) { + // Parse the request origin using registered origin parser. + String origin = parseOrigin(sRequest); + String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target; + ContextUtil.enter(contextName, origin); + + if (httpMethodSpecify) { + // Add HTTP method prefix if necessary. + String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target; + urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } else { + urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } + } + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse) response; + // Return the block page, or redirect to another URL. + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); + } catch (IOException | ServletException | RuntimeException e2) { + Tracer.traceEntry(e2, urlEntry); + throw e2; + } finally { + if (urlEntry != null) { + urlEntry.exit(); + } + ContextUtil.exit(); + } + } + + private String parseOrigin(HttpServletRequest request) { + RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser(); + String origin = EMPTY_ORIGIN; + if (originParser != null) { + origin = originParser.parseOrigin(request); + if (StringUtil.isEmpty(origin)) { + return EMPTY_ORIGIN; + } + } + return origin; + } + + @Override + public void destroy() { + + } + + private static final String EMPTY_ORIGIN = ""; +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java new file mode 100755 index 0000000000..a124eb826c --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2018 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.servlet; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * Servlet filter for all requests. + * + * @author quguai.ly + */ +public class CommonTotalFilter implements Filter { + + public static final String TOTAL_URL_REQUEST = "total-url-request"; + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest)request; + + Entry entry = null; + try { + ContextUtil.enter(WebServletConfig.WEB_SERVLET_CONTEXT_NAME); + entry = SphU.entry(TOTAL_URL_REQUEST, ResourceTypeConstants.COMMON_WEB); + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse)response; + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); + } catch (IOException | ServletException | RuntimeException e2) { + Tracer.trace(e2); + throw e2; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java new file mode 100755 index 0000000000..06b222c52d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * The default {@link UrlBlockHandler}. + * + * @author youji.zj + * @author quguai.ly + */ +public class DefaultUrlBlockHandler implements UrlBlockHandler { + + @Override + public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) + throws IOException { + // Directly redirect to the default flow control (blocked) page or customized block page. + FilterUtil.blockRequest(request, response); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java new file mode 100755 index 0000000000..a986e47655 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java @@ -0,0 +1,28 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +/*** + * @author youji.zj + * @author quguai.ly + */ +public class DefaultUrlCleaner implements UrlCleaner { + + @Override + public String clean(String originUrl) { + return originUrl; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java new file mode 100644 index 0000000000..9fc306e3df --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author Eric Zhao + * @author quguai.ly + * @since 0.2.0 + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(HttpServletRequest request); +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java new file mode 100755 index 0000000000..34a8ef9c62 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * The URL block handler handles requests when blocked. + * + * @author youji.zj + * @author quguai.ly + */ +public interface UrlBlockHandler { + + /** + * Handle the request when blocked. + * + * @param request Servlet request + * @param response Servlet response + * @param ex the block exception. + * @throws IOException some error occurs + */ + void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException; +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java new file mode 100755 index 0000000000..15c17da695 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java @@ -0,0 +1,32 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +/*** + * @author youji.zj + * @author quguai.ly + */ +public interface UrlCleaner { + + /*** + *

Process the url. Some path variables should be handled and unified.

+ *

e.g. collect_item_relation--10200012121-.html will be converted to collect_item_relation.html

+ * + * @param originUrl original url + * @return processed url + */ + String clean(String originUrl); +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java new file mode 100755 index 0000000000..90ccfffbfe --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999-2018 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.servlet.callback; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * Registry for URL cleaner and URL block handler. + * + * @author youji.zj + * @author quguai.ly + */ +public class WebCallbackManager { + + /** + * URL cleaner. + */ + private static volatile UrlCleaner urlCleaner = new DefaultUrlCleaner(); + + /** + * URL block handler. + */ + private static volatile UrlBlockHandler urlBlockHandler = new DefaultUrlBlockHandler(); + + private static volatile RequestOriginParser requestOriginParser = null; + + public static UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public static void setUrlCleaner(UrlCleaner urlCleaner) { + WebCallbackManager.urlCleaner = urlCleaner; + } + + public static UrlBlockHandler getUrlBlockHandler() { + return urlBlockHandler; + } + + public static void setUrlBlockHandler(UrlBlockHandler urlBlockHandler) { + AssertUtil.isTrue(urlBlockHandler != null, "URL block handler should not be null"); + WebCallbackManager.urlBlockHandler = urlBlockHandler; + } + + public static RequestOriginParser getRequestOriginParser() { + return requestOriginParser; + } + + public static void setRequestOriginParser(RequestOriginParser requestOriginParser) { + WebCallbackManager.requestOriginParser = requestOriginParser; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java new file mode 100755 index 0000000000..dd6c0fa721 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java @@ -0,0 +1,95 @@ +/* + * Copyright 1999-2018 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.servlet.config; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import com.alibaba.csp.sentinel.adapter.servlet.CommonTotalFilter; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * The configuration center for Web Servlet adapter. + * + * @author leyou + * @author zhaoyuguang + * @author quguai.ly + */ +public final class WebServletConfig { + + public static final String WEB_SERVLET_CONTEXT_NAME = "sentinel_web_servlet_context"; + + public static final String BLOCK_PAGE_URL_CONF_KEY = "csp.sentinel.web.servlet.block.page"; + public static final String BLOCK_PAGE_HTTP_STATUS_CONF_KEY = "csp.sentinel.web.servlet.block.status"; + + private static final int HTTP_STATUS_TOO_MANY_REQUESTS = 429; + + /** + * Get redirecting page when Sentinel blocking for {@link CommonFilter} or + * {@link CommonTotalFilter} occurs. + * + * @return the block page URL, maybe null if not configured. + */ + public static String getBlockPage() { + return SentinelConfig.getConfig(BLOCK_PAGE_URL_CONF_KEY); + } + + public static void setBlockPage(String blockPage) { + SentinelConfig.setConfig(BLOCK_PAGE_URL_CONF_KEY, blockPage); + } + + /** + *

Get the HTTP status when using the default block page.

+ *

You can set the status code with the {@code -Dcsp.sentinel.web.servlet.block.status} + * property. When the property is empty or invalid, Sentinel will use 429 (Too Many Requests) + * as the default status code.

+ * + * @return the HTTP status of the default block page + * @since 1.7.0 + */ + public static int getBlockPageHttpStatus() { + String value = SentinelConfig.getConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY); + if (StringUtil.isEmpty(value)) { + return HTTP_STATUS_TOO_MANY_REQUESTS; + } + try { + int s = Integer.parseInt(value); + if (s <= 0) { + throw new IllegalArgumentException("Invalid status code: " + s); + } + return s; + } catch (Exception e) { + RecordLog.warn("[WebServletConfig] Invalid block HTTP status (" + value + "), using default 429"); + setBlockPageHttpStatus(HTTP_STATUS_TOO_MANY_REQUESTS); + } + return HTTP_STATUS_TOO_MANY_REQUESTS; + } + + /** + * Set the HTTP status of the default block page. + * + * @param httpStatus the HTTP status of the default block page + * @since 1.7.0 + */ + public static void setBlockPageHttpStatus(int httpStatus) { + if (httpStatus <= 0) { + throw new IllegalArgumentException("Invalid HTTP status code: " + httpStatus); + } + SentinelConfig.setConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY, String.valueOf(httpStatus)); + } + + private WebServletConfig() {} +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java new file mode 100755 index 0000000000..220cbfb97a --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java @@ -0,0 +1,192 @@ +/* + * Copyright 1999-2018 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.servlet.util; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Util class for web servlet filter. + * + * @author zhaoyuguang + * @author youji.zj + * @author Eric Zhao + * @author quguai.ly + */ +public final class FilterUtil { + + private static final String PATH_SPLIT = "/"; + + public static String filterTarget(HttpServletRequest request) { + String pathInfo = getResourcePath(request); + if (!pathInfo.startsWith(PATH_SPLIT)) { + pathInfo = PATH_SPLIT + pathInfo; + } + + if (PATH_SPLIT.equals(pathInfo)) { + return pathInfo; + } + + // Note: pathInfo should be converted to camelCase style. + int lastSlashIndex = pathInfo.lastIndexOf("/"); + + if (lastSlashIndex >= 0) { + pathInfo = pathInfo.substring(0, lastSlashIndex) + "/" + + StringUtil.trim(pathInfo.substring(lastSlashIndex + 1)); + } else { + pathInfo = PATH_SPLIT + StringUtil.trim(pathInfo); + } + + return pathInfo; + } + + public static void blockRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + if (StringUtil.isBlank(WebServletConfig.getBlockPage())) { + writeDefaultBlockedPage(response, WebServletConfig.getBlockPageHttpStatus()); + } else { + String redirectUrl = WebServletConfig.getBlockPage() + "?http_referer=" + url.toString(); + // Redirect to the customized block page. + response.sendRedirect(redirectUrl); + } + } + + private static void writeDefaultBlockedPage(HttpServletResponse response, int httpStatus) throws IOException { + response.setStatus(httpStatus); + PrintWriter out = response.getWriter(); + out.print(DEFAULT_BLOCK_MSG); + out.flush(); + out.close(); + } + + private static String getResourcePath(HttpServletRequest request) { + String pathInfo = normalizeAbsolutePath(request.getPathInfo(), false); + String servletPath = normalizeAbsolutePath(request.getServletPath(), pathInfo.length() != 0); + + return servletPath + pathInfo; + } + + private static String normalizeAbsolutePath(String path, boolean removeTrailingSlash) throws IllegalStateException { + return normalizePath(path, true, false, removeTrailingSlash); + } + + private static String normalizePath(String path, boolean forceAbsolute, boolean forceRelative, + boolean removeTrailingSlash) throws IllegalStateException { + char[] pathChars = StringUtil.trimToEmpty(path).toCharArray(); + int length = pathChars.length; + + // Check path and slash. + boolean startsWithSlash = false; + boolean endsWithSlash = false; + + if (length > 0) { + char firstChar = pathChars[0]; + char lastChar = pathChars[length - 1]; + + startsWithSlash = firstChar == PATH_SPLIT.charAt(0) || firstChar == '\\'; + endsWithSlash = lastChar == PATH_SPLIT.charAt(0) || lastChar == '\\'; + } + + StringBuilder buf = new StringBuilder(length); + boolean isAbsolutePath = forceAbsolute || !forceRelative && startsWithSlash; + int index = startsWithSlash ? 0 : -1; + int level = 0; + + if (isAbsolutePath) { + buf.append(PATH_SPLIT); + } + + while (index < length) { + index = indexOfSlash(pathChars, index + 1, false); + + if (index == length) { + break; + } + + int nextSlashIndex = indexOfSlash(pathChars, index, true); + + String element = new String(pathChars, index, nextSlashIndex - index); + index = nextSlashIndex; + + // Ignore "." + if (".".equals(element)) { + continue; + } + + // Backtrack ".." + if ("..".equals(element)) { + if (level == 0) { + if (isAbsolutePath) { + throw new IllegalStateException(path); + } else { + buf.append("..").append(PATH_SPLIT); + } + } else { + buf.setLength(pathChars[--level]); + } + + continue; + } + + pathChars[level++] = (char)buf.length(); + buf.append(element).append(PATH_SPLIT); + } + + // remove the last "/" + if (buf.length() > 0) { + if (!endsWithSlash || removeTrailingSlash) { + buf.setLength(buf.length() - 1); + } + } + + return buf.toString(); + } + + private static int indexOfSlash(char[] chars, int beginIndex, boolean slash) { + int i = beginIndex; + + for (; i < chars.length; i++) { + char ch = chars[i]; + + if (slash) { + if (ch == PATH_SPLIT.charAt(0) || ch == '\\') { + break; // if a slash + } + } else { + if (ch != PATH_SPLIT.charAt(0) && ch != '\\') { + break; // if not a slash + } + } + } + + return i; + } + + public static final String DEFAULT_BLOCK_MSG = "Blocked by Sentinel (flow limiting)"; + + private FilterUtil() {} +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java new file mode 100644 index 0000000000..7b7b19a3bf --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilterTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 1999-2018 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.servlet; + +import java.util.Collections; + +import jakarta.servlet.http.HttpServletRequest; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.adapter.servlet.callback.DefaultUrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * @author zhaoyuguang + * @author Eric Zhao + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class) +@AutoConfigureMockMvc +public class CommonFilterTest { + + private static final String HELLO_STR = "Hello!"; + + @Autowired + private MockMvc mvc; + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @Test + public void testCommonFilterMiscellaneous() throws Exception { + Constants.ROOT.removeChildList(); + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals("", context); + + testCommonBlockAndRedirectBlockPage(url, cn); + + // Test for url cleaner. + testUrlCleaner(); + testUrlExclusion(); + testCustomOriginParser(); + } + + private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cn) throws Exception { + configureRulesFor(url, 0); + // The request will be blocked and response is default block message. + WebServletConfig.setBlockPageHttpStatus(HttpStatus.OK.value()); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + assertEquals(1, cn.blockQps(), 0.01); + + WebServletConfig.setBlockPageHttpStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isTooManyRequests()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + + // Test for redirect. + String redirectUrl = "http://some-location.com"; + WebServletConfig.setBlockPage(redirectUrl); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().is3xxRedirection()) + .andExpect(header().string("Location", redirectUrl + "?http_referer=http://localhost/hello")); + + FlowRuleManager.loadRules(null); + WebServletConfig.setBlockPage(""); + } + + private void testUrlCleaner() throws Exception { + final String fooPrefix = "/foo/"; + String url1 = fooPrefix + 1; + String url2 = fooPrefix + 2; + WebCallbackManager.setUrlCleaner(new UrlCleaner() { + @Override + public String clean(String originUrl) { + if (originUrl.startsWith(fooPrefix)) { + return "/foo/*"; + } + return originUrl; + } + }); + this.mvc.perform(get(url1).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string("Hello 1")); + this.mvc.perform(get(url2).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string("Hello 2")); + ClusterNode cn = ClusterBuilderSlot.getClusterNode(fooPrefix + "*"); + assertEquals(2, cn.passQps(), 0.01); + assertNull(ClusterBuilderSlot.getClusterNode(url1)); + assertNull(ClusterBuilderSlot.getClusterNode(url2)); + + WebCallbackManager.setUrlCleaner(new DefaultUrlCleaner()); + } + + private void testUrlExclusion() throws Exception { + final String excludePrefix = "/exclude/"; + String url = excludePrefix + 1; + WebCallbackManager.setUrlCleaner(new UrlCleaner() { + @Override + public String clean(String originUrl) { + if(originUrl.startsWith(excludePrefix)) { + return ""; + } + return originUrl; + } + }); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string("Exclude 1")); + assertNull(ClusterBuilderSlot.getClusterNode(url)); + WebCallbackManager.setUrlCleaner(new DefaultUrlCleaner()); + } + + private void testCustomOriginParser() throws Exception { + String url = "/hello"; + String limitOrigin = "userA"; + final String headerName = "S-User"; + configureRulesFor(url, 0, limitOrigin); + + WebCallbackManager.setRequestOriginParser(new RequestOriginParser() { + @Override + public String parseOrigin(HttpServletRequest request) { + String origin = request.getHeader(headerName); + return origin != null ? origin : ""; + } + }); + + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN).header(headerName, "userB")) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + // This will be blocked. + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN).header(headerName, limitOrigin)) + .andExpect(status().isTooManyRequests()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + WebCallbackManager.setRequestOriginParser(null); + FlowRuleManager.loadRules(null); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/FilterConfig.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/FilterConfig.java new file mode 100644 index 0000000000..119c6b9bcd --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/FilterConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 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.servlet; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Eric Zhao + */ +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestApplication.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestApplication.java new file mode 100644 index 0000000000..01913205a6 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2018 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.servlet; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Eric Zhao + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java new file mode 100644 index 0000000000..dce4e2ebfe --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servlet/TestController.java @@ -0,0 +1,47 @@ +/* + * Copyright 1999-2018 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.servlet; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Eric Zhao + */ +@RestController +public class TestController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } + + @GetMapping("/err") + public String apiError() { + return "Oops..."; + } + + @GetMapping("/foo/{id}") + public String apiFoo(@PathVariable("id") Long id) { + return "Hello " + id; + } + + @GetMapping("/exclude/{id}") + public String apiExclude(@PathVariable("id") Long id) { + return "Exclude " + id; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java new file mode 100644 index 0000000000..10c215d995 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/CommonFilterContextTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 1999-2018 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.servletcontext; + +import com.alibaba.csp.sentinel.Constants; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author zhaoyuguang + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestContextApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class CommonFilterContextTest { + + private static final String HELLO_STR = "Hello!"; + + @Autowired + private MockMvc mvc; + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @Test + public void testCommonFilterMiscellaneous() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cn = ClusterBuilderSlot.getClusterNode(url); + assertNotNull(cn); + assertEquals(1, cn.passQps(), 0.01); + String context = ""; + for (Node n : Constants.ROOT.getChildList()) { + if (n instanceof EntranceNode) { + String id = ((EntranceNode) n).getId().getName(); + if (url.equals(id)) { + context = ((EntranceNode) n).getId().getName(); + } + } + } + assertEquals(url, context); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java new file mode 100644 index 0000000000..aff3a5db9d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/FilterContextConfig.java @@ -0,0 +1,40 @@ +/* + * Copyright 1999-2018 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.servletcontext; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author zhaoyuguang + */ +@Configuration +public class FilterContextConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java new file mode 100644 index 0000000000..fbf16c6ee7 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2018 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.servletcontext; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author zhaoyuguang + */ +@SpringBootApplication +public class TestContextApplication { + + public static void main(String[] args) { + SpringApplication.run(TestContextApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java new file mode 100644 index 0000000000..39fc81a104 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletcontext/TestContextController.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 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.servletcontext; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author zhaoyuguang + */ +@RestController +public class TestContextController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java new file mode 100644 index 0000000000..b220961032 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/CommonFilterMethodTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 1999-2018 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.servletmethod; + +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * @author zhaoyuguang + * @author Roger Law + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class CommonFilterMethodTest { + + private static final String HELLO_STR = "Hello!"; + + private static final String HELLO_POST_STR = "Hello Post!"; + + private static final String GET = "GET"; + + private static final String POST = "POST"; + + private static final String COLON = ":"; + + @Autowired + private MockMvc mvc; + + private void configureRulesFor(String resource, int count) { + configureRulesFor(resource, count, "default"); + } + + private void configureRulesFor(String resource, int count, String limitApp) { + FlowRule rule = new FlowRule() + .setCount(count) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + rule.setResource(resource); + if (StringUtil.isNotBlank(limitApp)) { + rule.setLimitApp(limitApp); + } + FlowRuleManager.loadRules(Collections.singletonList(rule)); + } + + @Test + public void testCommonFilterMiscellaneous() throws Exception { + String url = "/hello"; + this.mvc.perform(get(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_STR)); + + ClusterNode cnGet = ClusterBuilderSlot.getClusterNode(GET + COLON + url); + assertNotNull(cnGet); + assertEquals(1, cnGet.passQps(), 0.01); + + + ClusterNode cnPost = ClusterBuilderSlot.getClusterNode(POST + COLON + url); + assertNull(cnPost); + + this.mvc.perform(post(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_POST_STR)); + + cnPost = ClusterBuilderSlot.getClusterNode(POST + COLON + url); + assertNotNull(cnPost); + assertEquals(1, cnPost.passQps(), 0.01); + + testCommonBlockAndRedirectBlockPage(url, cnGet, cnPost); + } + + private void testCommonBlockAndRedirectBlockPage(String url, ClusterNode cnGet, ClusterNode cnPost) throws Exception { + configureRulesFor(GET + ":" + url, 0); + // The request will be blocked and response is default block message. + this.mvc.perform(get(url).accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isTooManyRequests()) + .andExpect(content().string(FilterUtil.DEFAULT_BLOCK_MSG)); + assertEquals(1, cnGet.blockQps(), 0.01); + + // Test for post pass + this.mvc.perform(post(url)) + .andExpect(status().isOk()) + .andExpect(content().string(HELLO_POST_STR)); + + assertEquals(2, cnPost.passQps(), 0.01); + + + FlowRuleManager.loadRules(null); + WebServletConfig.setBlockPage(""); + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.resetClusterNodes(); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java new file mode 100644 index 0000000000..5759a75977 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/FilterMethodConfig.java @@ -0,0 +1,25 @@ +package com.alibaba.csp.sentinel.adapter.servletmethod; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author: Roger Law + **/ +@Configuration +public class FilterMethodConfig { + + @Bean + public FilterRegistrationBean sentinelFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new CommonFilter()); + registration.addUrlPatterns("/*"); + registration.addInitParameter(CommonFilter.HTTP_METHOD_SPECIFY, "true"); + registration.setName("sentinelFilter"); + registration.setOrder(1); + + return registration; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java new file mode 100644 index 0000000000..745d79a045 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999-2018 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.servletmethod; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Eric Zhao + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java new file mode 100644 index 0000000000..04b08b40ad --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-6x/src/test/java/com/alibaba/csp/sentinel/adapter/servletmethod/TestMethodController.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-2018 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.servletmethod; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Roger Law + */ +@RestController +public class TestMethodController { + + @GetMapping("/hello") + public String apiHello() { + return "Hello!"; + } + + @PostMapping("/hello") + public String apiHelloPost() { + return "Hello Post!"; + } + +}