From 7148b93cca72b507708850f93e9313e1ed5cd6c1 Mon Sep 17 00:00:00 2001 From: hongqiaowei Date: Mon, 30 Nov 2020 17:53:48 +0800 Subject: [PATCH] feat: new route --- src/main/java/we/filter/RouteFilter.java | 36 +-- src/main/java/we/plugin/auth/ApiConfig.java | 16 + .../auth/GatewayGroup2appsToApiConfig.java | 6 +- .../java/we/plugin/auth/ServiceConfig.java | 7 - src/main/java/we/util/UrlTransformUtils.java | 296 ++++++++++++++++++ src/main/java/we/util/WebUtils.java | 21 +- 6 files changed, 350 insertions(+), 32 deletions(-) create mode 100644 src/main/java/we/util/UrlTransformUtils.java diff --git a/src/main/java/we/filter/RouteFilter.java b/src/main/java/we/filter/RouteFilter.java index 89c687a..89bb7df 100644 --- a/src/main/java/we/filter/RouteFilter.java +++ b/src/main/java/we/filter/RouteFilter.java @@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -36,6 +37,7 @@ import we.legacy.RespEntity; import we.plugin.auth.ApiConfig; import we.plugin.auth.AuthPluginFilter; import we.proxy.FizzWebClient; +import we.util.Constants; import we.util.ThreadContext; import we.util.WebUtils; @@ -104,26 +106,24 @@ public class RouteFilter extends ProxyAggrFilter { ); } - ApiConfig ac = null; - Object authRes = WebUtils.getFilterResultDataItem(exchange, AuthPluginFilter.AUTH_PLUGIN_FILTER, AuthPluginFilter.RESULT); - if (authRes instanceof ApiConfig) { - ac = (ApiConfig) authRes; - } + String reqPath = WebUtils.getReqPath(exchange); + String rid = clientReq.getId(); + + ApiConfig ac = WebUtils.getApiConfig(exchange); + if (ac.type == ApiConfig.Type.SERVICE_DISCOVERY) { + String relativeUri = WebUtils.appendQuery(ac.transform(reqPath), exchange); + return send(exchange, ac.backendService, relativeUri, hdrs); + + } else if (ac.type == ApiConfig.Type.REVERSE_PROXY) { + String relativeUri = ac.getNextHttpHostPort() + WebUtils.appendQuery(ac.transform(reqPath), exchange); + return fizzWebClient.send(rid, clientReq.getMethod(), relativeUri, hdrs, clientReq.getBody()).flatMap(genServerResponse(exchange)); - String relativeUri = WebUtils.getRelativeUri(exchange); - if (ac == null || ac.type == ApiConfig.DIRECT_PROXY_MODE) { - return send(exchange, WebUtils.getServiceId(exchange), relativeUri, hdrs); } else { - String realUri; - String backendUrl = ac.getNextBackendUrl(); - int acpLen = ac.path.length(); - if (acpLen == 1) { - realUri = backendUrl + relativeUri; - } else { - realUri = backendUrl + relativeUri.substring(acpLen); - } - relativeUri.substring(acpLen); - return fizzWebClient.send(clientReq.getId(), clientReq.getMethod(), realUri, hdrs, clientReq.getBody()).flatMap(genServerResponse(exchange)); + String err = "cant handle api config type " + ac.type; + StringBuilder b = ThreadContext.getStringBuilder(); + WebUtils.request2stringBuilder(exchange, b); + log.error(b.append(Constants.Symbol.LF).append(err).toString(), LogService.BIZ_ID, rid); + return WebUtils.buildJsonDirectResponseAndBindContext(exchange, HttpStatus.OK, null, RespEntity.toJson(HttpStatus.INTERNAL_SERVER_ERROR.value(), err, rid)); } } diff --git a/src/main/java/we/plugin/auth/ApiConfig.java b/src/main/java/we/plugin/auth/ApiConfig.java index 38a47ca..f41af84 100644 --- a/src/main/java/we/plugin/auth/ApiConfig.java +++ b/src/main/java/we/plugin/auth/ApiConfig.java @@ -17,11 +17,13 @@ package we.plugin.auth; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpMethod; import we.plugin.PluginConfig; import we.util.JacksonUtils; +import we.util.UrlTransformUtils; import java.util.Arrays; import java.util.List; @@ -163,6 +165,20 @@ public class ApiConfig { // return backendUrls.get(idx % backendUrls.size()); // } + @JsonIgnore + public String getNextHttpHostPort() { + int idx = counter.incrementAndGet(); + if (idx < 0) { + counter.set(0); + idx = 0; + } + return httpHostPorts.get(idx % httpHostPorts.size()); + } + + public String transform(String reqPath) { + return UrlTransformUtils.transform(path, backendPath, reqPath); + } + @Override public String toString() { return JacksonUtils.writeValueAsString(this); diff --git a/src/main/java/we/plugin/auth/GatewayGroup2appsToApiConfig.java b/src/main/java/we/plugin/auth/GatewayGroup2appsToApiConfig.java index 91ff728..504f503 100644 --- a/src/main/java/we/plugin/auth/GatewayGroup2appsToApiConfig.java +++ b/src/main/java/we/plugin/auth/GatewayGroup2appsToApiConfig.java @@ -51,7 +51,7 @@ public class GatewayGroup2appsToApiConfig { } for (String a : ac.apps) { app2apiConfigMap.put(a, ac); - log.info(gg + " add " + a + " -> " + ac); + log.info("expose " + ac + " to " + gg + " group and " + a + " app"); } } } @@ -62,7 +62,7 @@ public class GatewayGroup2appsToApiConfig { if (app2apiConfigMap != null) { for (String a : ac.apps) { ApiConfig r = app2apiConfigMap.remove(a); - log.info(gg + " remove " + a + " -> " + r); + log.info("remove " + r + " from " + gg + " group and " + a + " app"); } } } @@ -77,7 +77,7 @@ public class GatewayGroup2appsToApiConfig { } for (String a : ac.apps) { ApiConfig old = app2apiConfigMap.put(a, ac); - log.info(gg + " update " + a + " -> " + old + " with " + ac); + log.info(gg + " group and " + a + " app update " + old + " with " + ac); } } } diff --git a/src/main/java/we/plugin/auth/ServiceConfig.java b/src/main/java/we/plugin/auth/ServiceConfig.java index 83916c5..36fcc62 100644 --- a/src/main/java/we/plugin/auth/ServiceConfig.java +++ b/src/main/java/we/plugin/auth/ServiceConfig.java @@ -127,13 +127,6 @@ public class ServiceConfig { private GatewayGroup2appsToApiConfig getApiConfig2(HttpMethod method, String reqPath) { - // List matchPathPatterns = (List) ThreadContext.get(mpps); - // if (matchPathPatterns == null) { - // matchPathPatterns = new ArrayList<>(); - // ThreadContext.set(mpps, matchPathPatterns); - // } - // matchPathPatterns.clear(); - List matchPathPatterns = ThreadContext.getArrayList(mpps, String.class); Set>> es = path2methodToApiConfigMapMap.entrySet(); diff --git a/src/main/java/we/util/UrlTransformUtils.java b/src/main/java/we/util/UrlTransformUtils.java new file mode 100644 index 0000000..0b17a1b --- /dev/null +++ b/src/main/java/we/util/UrlTransformUtils.java @@ -0,0 +1,296 @@ +package we.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Fizz gateway url transform util class + * + * @author zhongjie + */ +public class UrlTransformUtils { + private UrlTransformUtils() {} + + private static final FizzGatewayUrlAntPathMatcher ANT_PATH_MATCHER = new FizzGatewayUrlAntPathMatcher(); + + /** + * transform the backend path to the real backend request path + * @param frontendPath frontend path + * @param backendPath backend path + * @param reqPath request path + * @return the transformed backend path + * @throws IllegalStateException when the request path does not match the frontend path pattern + * @throws IllegalArgumentException The number of capturing groups in the pattern segment does not match the number of URI template variables it defines + */ + public static String transform(String frontendPath, String backendPath, String reqPath) { + Assert.hasText(frontendPath, "frontend path cannot be null"); + Assert.hasText(backendPath, "backend path cannot be null"); + Assert.hasText(reqPath, "req path cannot be null"); + Map variables = ANT_PATH_MATCHER.extractUriTemplateVariables(frontendPath, reqPath); + for (Map.Entry entry : variables.entrySet()) { + backendPath = backendPath.replaceAll("\\{" + Matcher.quoteReplacement(entry.getKey()) + "}", entry.getValue()); + } + + if (backendPath.indexOf('{') != -1) { + backendPath = backendPath.replaceAll("\\{[^/]*}", ""); + } + + return backendPath; + } + + /** + * 自定义Ant风格路径匹配器 + * 设置默认路径分隔符为{@code #} + * 使用{@link FizzGatewayUrlAntPathMatcher.FizzGatewayAntPathStringMatcher}设置自定义的参数变量值(额外返回变量名为$1...n的键值对) + * + * @author zhongjie + */ + static class FizzGatewayUrlAntPathMatcher extends AntPathMatcher { + private static final String DEFAULT_PATH_SEPARATOR = "#"; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private volatile Boolean cachePatterns; + + private final Map replaceDoubleStarPatternCache = new ConcurrentHashMap<>(256); + + private final Map tokenizedPatternCache = new ConcurrentHashMap<>(256); + + final Map stringMatcherCache = new ConcurrentHashMap<>(256); + + private boolean caseSensitive = true; + + private static AntPathMatcher DEFAULT_ANT_PATH_MATCHER = new AntPathMatcher(); + + public FizzGatewayUrlAntPathMatcher() { + // 设置默认路径分隔符为# + super(DEFAULT_PATH_SEPARATOR); + } + + @Override + public void setPathSeparator(String pathSeparator) { + throw new RuntimeException("operation not support"); + } + + @Override + public void setTrimTokens(boolean trimTokens) { + throw new RuntimeException("operation not support"); + } + + @Override + public void setCaseSensitive(boolean caseSensitive) { + super.setCaseSensitive(caseSensitive); + this.caseSensitive = caseSensitive; + } + + @Override + public void setCachePatterns(boolean cachePatterns) { + super.setCachePatterns(cachePatterns); + this.cachePatterns = cachePatterns; + } + + @Override + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new FizzGatewayAntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + @Override + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + this.replaceDoubleStarPatternCache.clear(); + } + + @Override + public String extractPathWithinPattern(String pattern, String path) { + return DEFAULT_ANT_PATH_MATCHER.extractPathWithinPattern(pattern, path); + } + + @Override + public String combine(String pattern1, String pattern2) { + return DEFAULT_ANT_PATH_MATCHER.combine(pattern1, pattern2); + } + + @Override + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { + String replaceDoubleStarPattern = null; + if (pattern != null) { + replaceDoubleStarPattern = getReplaceDoubleStarPattern(pattern); + } + return super.doMatch(replaceDoubleStarPattern, path, fullMatch, uriTemplateVariables); + } + + private String getReplaceDoubleStarPattern(String pattern) { + String replaceDoubleStarPattern = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns) { + replaceDoubleStarPattern = this.replaceDoubleStarPatternCache.get(pattern); + } + if (replaceDoubleStarPattern == null) { + // by-zhongjie 替换**为.*正则模式 + replaceDoubleStarPattern = pattern.replaceAll("/\\*\\*$", "/{\\$:.*}") + .replaceAll("/\\*\\*/", "/{\\$:.*}/") + .replaceAll("^\\*\\*/", "{\\$:.*}/"); + if (cachePatterns == null && this.replaceDoubleStarPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return replaceDoubleStarPattern; + } + if (cachePatterns == null || cachePatterns) { + this.replaceDoubleStarPatternCache.put(pattern, replaceDoubleStarPattern); + } + } + return replaceDoubleStarPattern; + } + + protected static class FizzGatewayAntPathStringMatcher extends AntPathStringMatcher { + // by-zhongjie 将 \?|\*|\{((?:\{[^/]+?\}|[^/{}]|\\[{}])+?)\} 改为 \?|\*|\{((?:\{[^/]+?\}|[^{}]|\\[{}])+?)\},排除/的限制 + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^{}]|\\\\[{}])+?)\\}"); + + // by-zhongjie 将 (.*) 改为 ([^/]*),限制变量只能匹配在非/的字符内 + private static final String DEFAULT_VARIABLE_PATTERN = "([^/]*)"; + + private final Pattern pattern; + + private final List variableNames = new LinkedList<>(); + + // by-zhongjie 匿名占位符 + private final String ANONYMOUS_PLACEHOLDER = "$"; + + public FizzGatewayAntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public FizzGatewayAntPathStringMatcher(String pattern, boolean caseSensitive) { + super(pattern, caseSensitive); + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + // by-zhongjie 对 ? 也使用模式匹配 + patternBuilder.append('('); + patternBuilder.append('.'); + patternBuilder.append(')'); + this.variableNames.add(ANONYMOUS_PLACEHOLDER); + } + else if ("*".equals(match)) { + // by-zhongjie 对 * 也使用模式匹配 + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(ANONYMOUS_PLACEHOLDER); + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + + @Override + public boolean matchStrings(String str, @Nullable Map uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + + if (!ANONYMOUS_PLACEHOLDER.equals(name)) { + uriTemplateVariables.put(name, value); + } + // by-zhongjie 对提取到的变量按序号输出 + uriTemplateVariables.put(ANONYMOUS_PLACEHOLDER + i, value); + } + } + return true; + } + else { + return false; + } + } + } + } +} diff --git a/src/main/java/we/util/WebUtils.java b/src/main/java/we/util/WebUtils.java index 8f1f486..675930f 100644 --- a/src/main/java/we/util/WebUtils.java +++ b/src/main/java/we/util/WebUtils.java @@ -36,6 +36,7 @@ import we.legacy.RespEntity; import we.plugin.auth.ApiConfig; import we.plugin.auth.AuthPluginFilter; +import java.net.URI; import java.util.*; /** @@ -247,18 +248,30 @@ public abstract class WebUtils { return path; } - public static String getRelativeUri(ServerWebExchange exchange) { - String relativeUri = getReqPath(exchange); - String qry = exchange.getRequest().getURI().getQuery(); + public static String getQuery(ServerWebExchange exchange) { + URI uri = exchange.getRequest().getURI(); + String qry = uri.getQuery(); if (qry != null) { if (StringUtils.indexOfAny(qry, Constants.Symbol.LEFT_BRACE, Constants.Symbol.FORWARD_SLASH, Constants.Symbol.HASH) > 0) { - qry = exchange.getRequest().getURI().getRawQuery(); + qry = uri.getRawQuery(); } + } + return qry; + } + + public static String getRelativeUri(ServerWebExchange exchange) { + String relativeUri = getReqPath(exchange); + String qry = getQuery(exchange); + if (qry != null) { relativeUri = relativeUri + Constants.Symbol.QUESTION + qry; } return relativeUri; } + public static String appendQuery(String path, ServerWebExchange exchange) { + return path + Constants.Symbol.QUESTION + getQuery(exchange); + } + public static Map getAppendHeaders(ServerWebExchange exchange) { return (Map) exchange.getAttributes().get(APPEND_HEADERS); }