feat: multi app support

This commit is contained in:
hongqiaowei
2021-01-26 15:21:30 +08:00
parent 9626c36832
commit a01064e24c
12 changed files with 347 additions and 136 deletions

View File

@@ -27,7 +27,7 @@ http://demo.fizzgate.com/
account/password:`admin`/`Aa123!`
health checking urlhttp://demo.fizzgate.com/serviceConfigs
health checking urlhttp://demo.fizzgate.com/admin/health
API accesshttp://demo.fizzgate.com/proxy/[Service Name]/[API Path]

View File

@@ -26,7 +26,7 @@ http://demo.fizzgate.com/
账号/密码:`admin`/`Aa123!`
健康检查地址http://demo.fizzgate.com/serviceConfigs
健康检查地址http://demo.fizzgate.com/admin/health
API地址http://demo.fizzgate.com/proxy/[服务名]/[API Path]

View File

@@ -34,7 +34,7 @@ import java.nio.charset.StandardCharsets;
*/
@RestController
@RequestMapping(value = "/config")
@RequestMapping(value = "/admin/config")
public class ConfigController {
@Resource

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import we.plugin.auth.ApiConfigService;
import we.plugin.auth.AppService;
import we.plugin.auth.GatewayGroupService;
import we.stats.ratelimit.ResourceRateLimitConfigService;
import we.util.Constants;
import we.util.DateTimeUtils;
import we.util.JacksonUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* @author hongqiaowei
*/
@RestController
@RequestMapping("/admin")
public class HealthCheckController {
@GetMapping("/health")
public Mono<String> health(ServerWebExchange exchange) {
long mills = System.currentTimeMillis();
String now = DateTimeUtils.toDate(mills, Constants.DatetimePattern.DP23);
return Mono.just(now + " ok");
}
}

View File

@@ -43,7 +43,7 @@ import java.util.stream.Collectors;
* @author zhongjie
*/
@RestController
@RequestMapping(value = "/managerConfig")
@RequestMapping(value = "/admin/managerConfig")
public class ManagerConfigController {
@NacosValue(value = "${fizz.manager.config.key:fizz-manager-key}", autoRefreshed = true)
@Value("${fizz.manager.config.key:fizz-manager-key}")

View File

@@ -53,10 +53,12 @@ public class ApiConfig {
private static final String match_all = "/**";
// @JsonIgnore
private static final int ENABLE = 1;
private static final int UNABLE = 0;
public int id; // tb_api_auth.id
// @JsonIgnore
public int isDeleted = 0; // tb_api_auth.is_deleted
public Set<String> gatewayGroups = Stream.of(GatewayGroup.DEFAULT).collect(Collectors.toSet());
@@ -67,28 +69,27 @@ public class ApiConfig {
public HttpMethod method = HttpMethod.X;
// public String path = String.valueOf(Constants.Symbol.FORWARD_SLASH);
public String path = match_all;
public boolean exactMatch = false;
public String backendPath;
public Set<String> apps = Stream.of(App.ALL_APP).collect(Collectors.toSet());
// public Set<String> apps = Stream.of(App.ALL_APP).collect(Collectors.toSet());
@JsonProperty("proxyMode")
public byte type = Type.SERVICE_DISCOVERY;
private AtomicInteger counter = new AtomicInteger(-1);
// public List<String> backendUrls;
public List<String> httpHostPorts;
public char access = ALLOW;
public List<PluginConfig> pluginConfigs;
public boolean checkApp = false;
public static boolean isAntPathPattern(String path) {
boolean uriVar = false;
for (int i = 0; i < path.length(); i++) {
@@ -120,18 +121,18 @@ public class ApiConfig {
}
}
public void setApp(String as) {
apps.remove(App.ALL_APP);
if (StringUtils.isBlank(as)) {
apps.add("*");
} else {
Arrays.stream(StringUtils.split(as, ',')).forEach(
a -> {
apps.add(a.trim());
}
);
}
}
// public void setApp(String as) {
// apps.remove(App.ALL_APP);
// if (StringUtils.isBlank(as)) {
// apps.add("*");
// } else {
// Arrays.stream(StringUtils.split(as, ',')).forEach(
// a -> {
// apps.add(a.trim());
// }
// );
// }
// }
public void setPath(String p) {
if (StringUtils.isNotBlank(p)) {
@@ -155,15 +156,13 @@ public class ApiConfig {
}
}
// @JsonIgnore
// public String getNextBackendUrl() {
// int idx = counter.incrementAndGet();
// if (idx < 0) {
// counter.set(0);
// idx = 0;
// }
// return backendUrls.get(idx % backendUrls.size());
// }
public void setAppEnable(int v) {
if (v == ENABLE) {
checkApp = true;
} else {
checkApp = false;
}
}
@JsonIgnore
public String getNextHttpHostPort() {
@@ -182,6 +181,20 @@ public class ApiConfig {
return UrlTransformUtils.transform(path, backendPath, reqPath);
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ApiConfig) {
ApiConfig that = (ApiConfig) obj;
return this.id == that.id;
}
return false;
}
@Override
public String toString() {
return JacksonUtils.writeValueAsString(this);

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.plugin.auth;
import we.util.JacksonUtils;
import java.util.Collections;
import java.util.List;
/**
* @author hongqiaowei
*/
public class ApiConfig2apps {
public static final int DELETED = 1;
public int id;
public int isDeleted = 0;
public List<String> apps;
@Override
public String toString() {
return JacksonUtils.writeValueAsString(this);
}
}

View File

@@ -18,9 +18,6 @@
package we.plugin.auth;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,12 +28,11 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import we.flume.clients.log4j2appender.LogService;
import we.config.AggregateRedisConfig;
import we.flume.clients.log4j2appender.LogService;
import we.util.*;
import javax.annotation.PostConstruct;
@@ -71,48 +67,6 @@ public class ApiConfigService {
private Map<Integer, ApiConfig> apiConfigMap = new HashMap<>(128);
// TODO XXX
@Value("${serviceWhiteList:x}")
private String serviceWhiteList;
private Set<String> whiteListSet = new HashSet<>(196);
@ApolloConfigChangeListener
private void configChangeListter(ConfigChangeEvent cce) {
cce.changedKeys().forEach(
k -> {
ConfigChange cc = cce.getChange(k);
if (cc.getPropertyName().equalsIgnoreCase("serviceWhiteList")) {
this.updateServiceWhiteList(cc.getOldValue(), cc.getNewValue());
}
}
);
}
private void updateServiceWhiteList(String oldValue, String newValue) {
if (ObjectUtils.nullSafeEquals(oldValue, newValue)) {
return;
}
log.info("old service white list: " + oldValue);
serviceWhiteList = newValue;
afterServiceWhiteListSet();
}
@NacosValue(value = "${serviceWhiteList:x}", autoRefreshed = true)
public void setServiceWhiteList(String serviceWhiteList) {
this.updateServiceWhiteList(this.serviceWhiteList, serviceWhiteList);
}
public void afterServiceWhiteListSet() {
if (StringUtils.isNotBlank(serviceWhiteList)) {
whiteListSet.clear();
Arrays.stream(StringUtils.split(serviceWhiteList, Constants.Symbol.COMMA)).forEach(s -> {
whiteListSet.add(s);
});
log.info("new service white list: " + whiteListSet.toString());
} else {
log.info("no service white list");
}
}
@NacosValue(value = "${need-auth:false}", autoRefreshed = true)
@Value("${need-auth:false}")
private boolean needAuth;
@@ -123,6 +77,9 @@ public class ApiConfigService {
@Resource
private AppService appService;
@Resource
private ApiConifg2appsService apiConifg2appsService;
@Resource
private GatewayGroupService gatewayGroupService;
@@ -136,8 +93,6 @@ public class ApiConfigService {
@PostConstruct
public void init() throws Throwable {
afterServiceWhiteListSet(); // TODO XXX
final Throwable[] throwable = new Throwable[1];
Throwable error = Mono.just(Objects.requireNonNull(rt.opsForHash().entries(fizzApiConfig)
.defaultIfEmpty(new AbstractMap.SimpleEntry<>(ReactorUtils.OBJ, ReactorUtils.OBJ)).onErrorStop().doOnError(t -> {
@@ -226,6 +181,7 @@ public class ApiConfigService {
log.info("no " + ac.service + " config to delete");
} else {
sc.remove(ac);
apiConifg2appsService.remove(ac.id);
}
} else {
if (sc == null) {
@@ -260,8 +216,6 @@ public class ApiConfigService {
CUSTOM_AUTH_REJECT ("custom auth reject"),
SERVICE_NOT_OPEN ("service not open"),
CANT_ACCESS_SERVICE_API ("cant access service api");
private String reason;
@@ -275,6 +229,21 @@ public class ApiConfigService {
}
}
public ApiConfig getApiConfig(String service, HttpMethod method, String path, String gatewayGroup, String app) {
ServiceConfig sc = serviceConfigMap.get(service);
if (sc != null) {
Set<ApiConfig> acs = sc.getApiConfigs(method, path, gatewayGroup);
if (acs != null) {
for (ApiConfig ac : acs) {
if (apiConifg2appsService.contains(ac.id, app)) {
return ac;
}
}
}
}
return null;
}
public Mono<Object> canAccess(ServerWebExchange exchange) {
ServerHttpRequest req = exchange.getRequest();
HttpHeaders hdrs = req.getHeaders();
@@ -286,11 +255,6 @@ public class ApiConfigService {
private Mono<Object> canAccess(ServerWebExchange exchange, String app, String ip, String timestamp, String sign, String secretKey,
String service, HttpMethod method, String path) {
// if (openServiceWhiteList) {
// if (!whiteListSet.contains(service)) { // TODO XXX
// return Mono.just(Access.SERVICE_NOT_OPEN);
// }
// }
ServiceConfig sc = serviceConfigMap.get(service);
if (sc == null) {
if (!needAuth) {
@@ -301,8 +265,8 @@ public class ApiConfigService {
} else {
String api = ThreadContext.getStringBuilder().append(service).append(Constants.Symbol.BLANK).append(method.name()).append(Constants.Symbol.BLANK + path).toString();
ApiConfig ac0 = null;
for (String g : gatewayGroupService.currentGatewayGroupSet) { // compatible
ac0 = sc.getApiConfig(method, path, g, app);
for (String g : gatewayGroupService.currentGatewayGroupSet) {
ac0 = getApiConfig(service, method, path, g, app);
if (ac0 != null) {
break;
}
@@ -315,9 +279,9 @@ public class ApiConfigService {
return logWarnAndResult(api + " no api config", Access.NO_API_CONFIG);
}
} else if (gatewayGroupService.currentGatewayGroupIn(ac.gatewayGroups)) {
if (ac.apps.contains(App.ALL_APP)) {
if (!ac.checkApp) {
return allow(api, ac);
} else if (app != null && ac.apps.contains(app)) {
} else if (app != null && apiConifg2appsService.contains(ac.id, app) ) {
if (ac.access == ApiConfig.ALLOW) {
App a = appService.getApp(app);
if (a.useWhiteList && !a.allow(ip)) {

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2020 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package we.plugin.auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Service;
import we.config.AggregateRedisConfig;
import we.flume.clients.log4j2appender.LogService;
import we.util.Constants;
import we.util.JacksonUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author hongqiaowei
*/
@Service
public class ApiConifg2appsService {
private static final Logger log = LoggerFactory.getLogger(ApiConifg2appsService.class);
private static final String fizzApiConfigAppChannel = "fizz_api_config_app_channel";
private Map<Integer/* api config id */, Set<String/* app */>> apiConfig2appsMap = new HashMap<>(128);
@Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE)
private ReactiveStringRedisTemplate rt;
@PostConstruct
public void init() throws Throwable {
rt.listenToChannel(fizzApiConfigAppChannel)
.doOnError(
t -> {
log.error("lsn api config 2 apps channel", t);
}
)
.doOnComplete(
() -> {
log.info("success to lsn on api config 2 apps channel");
}
)
.doOnNext(
msg -> {
String json = msg.getMessage();
log.info(json, LogService.BIZ_ID, "ac2as" + System.currentTimeMillis());
try {
ApiConfig2apps data = JacksonUtils.readValue(json, ApiConfig2apps.class);
updateApiConfig2appsMap(data);
} catch (Throwable t) {
log.error(Constants.Symbol.EMPTY, t);
}
}
)
.subscribe()
;
}
private void updateApiConfig2appsMap(ApiConfig2apps data) {
Set<String> apps = apiConfig2appsMap.get(data.id);
if (data.isDeleted == ApiConfig2apps.DELETED) {
if (apps != null) {
apps.removeAll(data.apps);
log.info("remove " + data.apps);
}
} else {
if (apps == null) {
apps = new HashSet<>(32);
apiConfig2appsMap.put(data.id, apps);
}
apps.addAll(data.apps);
log.info("add " + data.apps);
}
}
public boolean contains(int api, String app) {
Set<String> apps = apiConfig2appsMap.get(api);
if (apps == null) {
return false;
} else {
return apps.contains(app);
}
}
public Set<String> remove(int id) {
return apiConfig2appsMap.remove(id);
}
}

View File

@@ -22,7 +22,9 @@ import org.slf4j.LoggerFactory;
import we.util.JacksonUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author hongqiaowei
@@ -30,65 +32,86 @@ import java.util.Map;
public class GatewayGroup2apiConfig {
private static final Logger log = LoggerFactory.getLogger(GatewayGroup2apiConfig.class);
// private static final Logger log = LoggerFactory.getLogger(GatewayGroup2apiConfig.class);
private Map<String/*gg*/, Map<String/*a*/, ApiConfig>> configMap = new HashMap<>(6);
// private Map<String/*gg*/, Map<String/*a*/, ApiConfig>> configMap = new HashMap<>(6);
private Map<String/*gg*/, Set<ApiConfig>> configMap = new HashMap<>(6);
public Map<String, Map<String, ApiConfig>> getConfigMap() {
return configMap;
}
// public Map<String, Map<String, ApiConfig>> getConfigMap() {
// return configMap;
// }
public void setConfigMap(Map<String, Map<String, ApiConfig>> configMap) {
this.configMap = configMap;
}
// public void setConfigMap(Map<String, Map<String, ApiConfig>> configMap) {
// this.configMap = configMap;
// }
public void add(ApiConfig ac) {
for (String gg : ac.gatewayGroups) {
Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
if (app2apiConfigMap == null) {
app2apiConfigMap = new HashMap<>();
configMap.put(gg, app2apiConfigMap);
}
for (String a : ac.apps) {
app2apiConfigMap.put(a, ac);
log.info("expose " + ac + " to " + gg + " group and " + a + " app");
// Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
// if (app2apiConfigMap == null) {
// app2apiConfigMap = new HashMap<>();
// configMap.put(gg, app2apiConfigMap);
// }
// for (String a : ac.apps) {
// app2apiConfigMap.put(a, ac);
// log.info("expose " + ac + " to " + gg + " group and " + a + " app");
// }
Set<ApiConfig> acs = configMap.get(gg);
if (acs == null) {
acs = new HashSet<>(6);
configMap.put(gg, acs);
}
acs.add(ac);
}
}
public void remove(ApiConfig ac) {
for (String gg : ac.gatewayGroups) {
Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
if (app2apiConfigMap != null) {
for (String a : ac.apps) {
ApiConfig r = app2apiConfigMap.remove(a);
log.info("remove " + r + " from " + gg + " group and " + a + " app");
}
// Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
// if (app2apiConfigMap != null) {
// for (String a : ac.apps) {
// ApiConfig r = app2apiConfigMap.remove(a);
// log.info("remove " + r + " from " + gg + " group and " + a + " app");
// }
// }
Set<ApiConfig> acs = configMap.get(gg);
if (acs != null) {
acs.remove(ac);
}
}
}
public void update(ApiConfig ac) {
for (String gg : ac.gatewayGroups) {
Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
if (app2apiConfigMap == null) {
app2apiConfigMap = new HashMap<>();
configMap.put(gg, app2apiConfigMap);
}
for (String a : ac.apps) {
ApiConfig old = app2apiConfigMap.put(a, ac);
log.info(gg + " group and " + a + " app update " + old + " with " + ac);
// Map<String, ApiConfig> app2apiConfigMap = configMap.get(gg);
// if (app2apiConfigMap == null) {
// app2apiConfigMap = new HashMap<>();
// configMap.put(gg, app2apiConfigMap);
// }
// for (String a : ac.apps) {
// ApiConfig old = app2apiConfigMap.put(a, ac);
// log.info(gg + " group and " + a + " app update " + old + " with " + ac);
// }
Set<ApiConfig> acs = configMap.get(gg);
if (acs == null) {
acs = new HashSet<>(6);
configMap.put(gg, acs);
}
acs.add(ac);
}
}
public ApiConfig get(String gatewayGroup, String app) {
Map<String, ApiConfig> app2apiConfigMap = configMap.get(gatewayGroup);
if (app2apiConfigMap == null) {
return null;
} else {
return app2apiConfigMap.get(app);
}
// public ApiConfig get(String gatewayGroup, String app) {
// Map<String, ApiConfig> app2apiConfigMap = configMap.get(gatewayGroup);
// if (app2apiConfigMap == null) {
// return null;
// } else {
// return app2apiConfigMap.get(app);
// }
// }
public Set<ApiConfig> get(String gatewayGroup) {
return configMap.get(gatewayGroup);
}
public String toString() {

View File

@@ -111,17 +111,25 @@ public class ServiceConfig {
}
}
// @JsonIgnore
// public ApiConfig getApiConfig(HttpMethod method, String path, String gatewayGroup, String app) {
// GatewayGroup2apiConfig r = getApiConfig(method, path);
// if (r == null) {
// return null;
// }
// if (StringUtils.isBlank(app)) {
// app = App.ALL_APP;
// }
// return r.get(gatewayGroup, app);
// }
@JsonIgnore
public ApiConfig getApiConfig(HttpMethod method, String path, String gatewayGroup, String app) {
// GatewayGroup2appsToApiConfig r = getApiConfig0(method, path);
public Set<ApiConfig> getApiConfigs(HttpMethod method, String path, String gatewayGroup) {
GatewayGroup2apiConfig r = getApiConfig(method, path);
if (r == null) {
return null;
}
if (StringUtils.isBlank(app)) {
app = App.ALL_APP;
}
return r.get(gatewayGroup, app);
return r.get(gatewayGroup);
}
private GatewayGroup2apiConfig getApiConfig(HttpMethod method, String reqPath) {

View File

@@ -24,8 +24,8 @@ public class WebUtilsTests {
MockServerHttpRequest mr = MockServerHttpRequest.get("http://127.0.0.1:8600/proxytest/test/ybiz").build();
MockServerWebExchange me = MockServerWebExchange.from(mr);
String cs = WebUtils.getClientService(me);
System.err.println(cs);
// System.err.println(cs);
String crpp = WebUtils.getClientReqPathPrefix(me);
System.err.println(crpp);
// System.err.println(crpp);
}
}