feat: new route
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +127,6 @@ public class ServiceConfig {
|
||||
|
||||
private GatewayGroup2appsToApiConfig getApiConfig2(HttpMethod method, String reqPath) {
|
||||
|
||||
// List<String> matchPathPatterns = (List<String>) ThreadContext.get(mpps);
|
||||
// if (matchPathPatterns == null) {
|
||||
// matchPathPatterns = new ArrayList<>();
|
||||
// ThreadContext.set(mpps, matchPathPatterns);
|
||||
// }
|
||||
// matchPathPatterns.clear();
|
||||
|
||||
List<String> matchPathPatterns = ThreadContext.getArrayList(mpps, String.class);
|
||||
|
||||
Set<Map.Entry<String, EnumMap<HttpMethod, GatewayGroup2appsToApiConfig>>> es = path2methodToApiConfigMapMap.entrySet();
|
||||
|
||||
296
src/main/java/we/util/UrlTransformUtils.java
Normal file
296
src/main/java/we/util/UrlTransformUtils.java
Normal file
@@ -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<String, String> variables = ANT_PATH_MATCHER.extractUriTemplateVariables(frontendPath, reqPath);
|
||||
for (Map.Entry<String, String> 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<String, String> replaceDoubleStarPatternCache = new ConcurrentHashMap<>(256);
|
||||
|
||||
private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<>(256);
|
||||
|
||||
final Map<String, AntPathStringMatcher> 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<String, String> 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<String> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> getAppendHeaders(ServerWebExchange exchange) {
|
||||
return (Map<String, String>) exchange.getAttributes().get(APPEND_HEADERS);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user