Api pairing service
This commit is contained in:
@@ -104,18 +104,28 @@ refresh-local-cache:
|
||||
rpc-service-enabled: true
|
||||
|
||||
fizz:
|
||||
|
||||
aggregate:
|
||||
writeMapNullValue: false
|
||||
|
||||
error:
|
||||
response:
|
||||
http-status.enable: true # default
|
||||
code-field: "msgCode" # default
|
||||
message-field: "message" # default
|
||||
http-status.enable: true
|
||||
code-field: "msgCode"
|
||||
message-field: "message"
|
||||
|
||||
api.pairing:
|
||||
enable: false
|
||||
web-server.port: 8601
|
||||
request:
|
||||
timeliness: 300 # default 300 sec
|
||||
timeout: 0 # default no timeout
|
||||
retry-count: 0 # default no retry
|
||||
retry-interval: 0 # default no retry interval
|
||||
|
||||
fizz-trace-id:
|
||||
header: X-Trace-Id # default
|
||||
header: X-Trace-Id
|
||||
value-strategy: requestId # default, or can be uuid
|
||||
value-prefix: fizz # default
|
||||
value-prefix: fizz
|
||||
|
||||
cors: true # CORS switch, default true
|
||||
|
||||
|
||||
@@ -34,22 +34,27 @@ public abstract class ThreadContext {
|
||||
private static final int mapCap = 32;
|
||||
|
||||
private static final String sb = "$sb";
|
||||
|
||||
public static final String sb0 = "$sb0";
|
||||
|
||||
private static final int sbCap = 256;
|
||||
|
||||
private static final String arrayListT = "arlstT";
|
||||
|
||||
private static final String arrayList = "arlstT";
|
||||
public static final String arrayList0 = "arlst0T";
|
||||
private static final String hashMap = "hsMapT";
|
||||
private static final String hashSet = "hsSetT";
|
||||
|
||||
private static final String hashMapT = "hsMapT";
|
||||
|
||||
private static final String hashSetT = "hsSetT";
|
||||
private static final String traId = "traIdT";
|
||||
|
||||
private ThreadContext() {
|
||||
}
|
||||
|
||||
public static void setTraceId(String traceId) {
|
||||
set(traId, traceId);
|
||||
}
|
||||
|
||||
public String getTraceId() {
|
||||
return (String) get(traId);
|
||||
}
|
||||
|
||||
/** use me carefully! */
|
||||
public static StringBuilder getStringBuilder() {
|
||||
return getStringBuilder(true);
|
||||
@@ -128,7 +133,7 @@ public abstract class ThreadContext {
|
||||
}
|
||||
|
||||
public static <T> ArrayList<T> getArrayList() {
|
||||
return getArrayList(arrayListT, true);
|
||||
return getArrayList(arrayList, true);
|
||||
}
|
||||
|
||||
public static <T> ArrayList<T> getArrayList(String key) {
|
||||
@@ -147,7 +152,7 @@ public abstract class ThreadContext {
|
||||
}
|
||||
|
||||
public static <K, V> HashMap<K, V> getHashMap() {
|
||||
return getHashMap(hashMapT, true);
|
||||
return getHashMap(hashMap, true);
|
||||
}
|
||||
|
||||
public static <K, V> HashMap<K, V> getHashMap(String key) {
|
||||
@@ -166,7 +171,7 @@ public abstract class ThreadContext {
|
||||
}
|
||||
|
||||
public static <E> HashSet<E> getHashSet() {
|
||||
return getHashSet(hashSetT, true);
|
||||
return getHashSet(hashSet, true);
|
||||
}
|
||||
|
||||
public static <E> HashSet<E> getHashSet(String key) {
|
||||
|
||||
@@ -25,5 +25,5 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
public class Fizz {
|
||||
|
||||
public static ConfigurableApplicationContext context;
|
||||
public static ConfigurableApplicationContext context; // TODO: rename to CONTEXT
|
||||
}
|
||||
|
||||
52
fizz-core/src/main/java/we/api/pairing/Api.java
Normal file
52
fizz-core/src/main/java/we/api/pairing/Api.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import we.plugin.auth.ApiConfig;
|
||||
import we.util.JacksonUtils;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class Api {
|
||||
|
||||
public Object method;
|
||||
|
||||
public String path;
|
||||
|
||||
public void setMethod(String m) {
|
||||
method = HttpMethod.resolve(m);
|
||||
if (method == null) {
|
||||
method = ApiConfig.ALL_METHOD;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean methodMatch(HttpMethod m) {
|
||||
if (method == ApiConfig.ALL_METHOD) {
|
||||
return true;
|
||||
}
|
||||
return method.equals(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JacksonUtils.writeValueAsString(this);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ package we.api.pairing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -31,14 +30,20 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import we.config.SystemConfig;
|
||||
import we.flume.clients.log4j2appender.LogService;
|
||||
import we.plugin.auth.App;
|
||||
import we.plugin.auth.AppService;
|
||||
import we.util.DateTimeUtils;
|
||||
import we.util.JacksonUtils;
|
||||
import we.util.ThreadContext;
|
||||
import we.util.WebUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
@@ -46,18 +51,18 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(SystemConfig.DEFAULT_GATEWAY_PREFIX + "/_fizz-pairing")
|
||||
public class FizzApiPairingController {
|
||||
public class ApiPairingController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FizzApiPairingController.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiPairingController.class);
|
||||
|
||||
@Resource
|
||||
private SystemConfig systemConfig;
|
||||
private SystemConfig systemConfig;
|
||||
|
||||
@Resource
|
||||
private AppService appService;
|
||||
private AppService appService;
|
||||
|
||||
@Value("${fizz.api.pairing.request.timeliness:300}")
|
||||
private int timeliness = 300; // unit: sec
|
||||
@Resource
|
||||
private ApiPairingDocSetService apiPairingDocSetService;
|
||||
|
||||
@GetMapping("/pair")
|
||||
public Mono<Void> pair(ServerWebExchange exchange) {
|
||||
@@ -84,6 +89,7 @@ public class FizzApiPairingController {
|
||||
try {
|
||||
long ts = Long.parseLong(timestamp);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long timeliness = systemConfig.fizzApiPairingRequestTimeliness();
|
||||
long start = DateTimeUtils.toMillis(now.minusSeconds(timeliness));
|
||||
long end = DateTimeUtils.toMillis(now.plusSeconds (timeliness));
|
||||
if (start <= ts && ts <= end) {
|
||||
@@ -100,17 +106,36 @@ public class FizzApiPairingController {
|
||||
return WebUtils.buildDirectResponse(response, null, null, "请求未签名");
|
||||
}
|
||||
|
||||
boolean equals = PairingUtils.checkSign(appId, timestamp, app.secretkey, sign);
|
||||
|
||||
boolean equals = ApiPairingUtils.checkSign(appId, timestamp, app.secretkey, sign);
|
||||
if (equals) {
|
||||
// TODO: 响应文档集
|
||||
return Mono.empty();
|
||||
List<AppApiPairingDocSet> docs = getAppDocSet(appId);
|
||||
String docsJson = JacksonUtils.writeValueAsString(docs);
|
||||
return response.writeWith(Mono.just(response.bufferFactory().wrap(docsJson.getBytes())));
|
||||
} else {
|
||||
log.warn("request authority: app {}, timestamp {}, sign {} invalid", appId, timestamp, sign);
|
||||
log.warn("{}request authority: app {}, timestamp {}, sign {} invalid",
|
||||
exchange.getLogPrefix(), appId, timestamp, sign, LogService.BIZ_ID, WebUtils.getTraceId(exchange));
|
||||
return WebUtils.buildDirectResponse(response, null, null, "请求签名无效");
|
||||
}
|
||||
}
|
||||
|
||||
private List<AppApiPairingDocSet> getAppDocSet(String appId) {
|
||||
Map<Integer, ApiPairingDocSet> docSetMap = apiPairingDocSetService.getDocSetMap();
|
||||
ArrayList<AppApiPairingDocSet> result = ThreadContext.getArrayList();
|
||||
for (Map.Entry<Integer, ApiPairingDocSet> entry : docSetMap.entrySet()) {
|
||||
ApiPairingDocSet ds = entry.getValue();
|
||||
AppApiPairingDocSet appDocSet = new AppApiPairingDocSet();
|
||||
appDocSet.id = ds.id;
|
||||
appDocSet.name = ds.name;
|
||||
appDocSet.services = ds.docs.stream().map(d -> d.service).collect(Collectors.toSet());
|
||||
appDocSet.enabled = false;
|
||||
if (ds.appIds.contains(appId)) {
|
||||
appDocSet.enabled = true;
|
||||
}
|
||||
result.add(appDocSet);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getSign(HttpHeaders headers) {
|
||||
List<String> signHdrs = systemConfig.getSignHeaders();
|
||||
for (int i = 0; i < signHdrs.size(); i++) {
|
||||
39
fizz-core/src/main/java/we/api/pairing/ApiPairingDoc.java
Normal file
39
fizz-core/src/main/java/we/api/pairing/ApiPairingDoc.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import we.util.JacksonUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class ApiPairingDoc {
|
||||
|
||||
public String service;
|
||||
|
||||
public List<Api> apis = Collections.emptyList();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JacksonUtils.writeValueAsString(this);
|
||||
}
|
||||
}
|
||||
62
fizz-core/src/main/java/we/api/pairing/ApiPairingDocSet.java
Normal file
62
fizz-core/src/main/java/we/api/pairing/ApiPairingDocSet.java
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import we.util.JacksonUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class ApiPairingDocSet {
|
||||
|
||||
public static final int DELETED = 1;
|
||||
|
||||
public int isDeleted = 0;
|
||||
|
||||
public int id; // 文档集 id
|
||||
|
||||
public String name;
|
||||
|
||||
public List<ApiPairingDoc> docs = Collections.emptyList();
|
||||
|
||||
public Set<String> appIds = Collections.emptySet();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ApiPairingDocSet that = (ApiPairingDocSet) o;
|
||||
return id == that.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JacksonUtils.writeValueAsString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import we.Fizz;
|
||||
import we.config.AggregateRedisConfig;
|
||||
import we.plugin.auth.ApiConfig;
|
||||
import we.util.JacksonUtils;
|
||||
import we.util.ReactiveResult;
|
||||
import we.util.Result;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
@Service
|
||||
public class ApiPairingDocSetService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiPairingDocSetService.class);
|
||||
|
||||
private Map<Integer/* doc set id */, ApiPairingDocSet> docSetMap = new HashMap<>(64);
|
||||
|
||||
private Map<String /* app id */, Set<ApiPairingDocSet>> appDocSetMap = new HashMap<>(64);
|
||||
|
||||
private Map<String /* service */, Set<ApiPairingDocSet>> serviceExistsInDocSetMap = new HashMap<>(64);
|
||||
|
||||
private Map<String /* path */, Map<Object /* method */, Set<ApiPairingDocSet>>> pathMethodExistsInDocSetMap = new HashMap<>(64);
|
||||
|
||||
@Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE)
|
||||
private ReactiveStringRedisTemplate rt;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws Throwable {
|
||||
Result<?> result = initApiPairingDocSet();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
result = lsnApiPairingDocSetChange();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
}
|
||||
|
||||
private Result<?> initApiPairingDocSet() {
|
||||
Result<?> result = Result.succ();
|
||||
Flux<Map.Entry<Object, Object>> resources = rt.opsForHash().entries("fizz_api_pairing_doc");
|
||||
resources.collectList()
|
||||
.defaultIfEmpty(Collections.emptyList())
|
||||
.flatMap(
|
||||
es -> {
|
||||
if (Fizz.context != null) {
|
||||
String json = null;
|
||||
try {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
json = (String) e.getValue();
|
||||
ApiPairingDocSet docSet = JacksonUtils.readValue(json, ApiPairingDocSet.class);
|
||||
updateDocSetDataStruct(docSet);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init api pairing doc set error, doc set: " + json;
|
||||
result.t = t;
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
)
|
||||
.onErrorReturn(
|
||||
throwable -> {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init api pairing doc set error";
|
||||
result.t = throwable;
|
||||
return true;
|
||||
},
|
||||
result
|
||||
)
|
||||
.block();
|
||||
return result;
|
||||
}
|
||||
|
||||
private Result<?> lsnApiPairingDocSetChange() {
|
||||
Result<?> result = Result.succ();
|
||||
String channel = "fizz_api_pairing_doc_channel";
|
||||
rt.listenToChannel(channel)
|
||||
.doOnError(
|
||||
t -> {
|
||||
result.code = ReactiveResult.FAIL;
|
||||
result.msg = "lsn error, channel: " + channel;
|
||||
result.t = t;
|
||||
log.error("lsn channel {} error", channel, t);
|
||||
}
|
||||
)
|
||||
.doOnSubscribe(
|
||||
s -> {
|
||||
log.info("success to lsn on {}", channel);
|
||||
}
|
||||
)
|
||||
.doOnNext(
|
||||
msg -> {
|
||||
if (Fizz.context != null) {
|
||||
String message = msg.getMessage();
|
||||
try {
|
||||
ApiPairingDocSet docSet = JacksonUtils.readValue(message, ApiPairingDocSet.class);
|
||||
updateDocSetDataStruct(docSet);
|
||||
} catch (Throwable t) {
|
||||
log.error("update api pairing doc set error, {}", message, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updateDocSetDataStruct(ApiPairingDocSet docSet) {
|
||||
if (docSet.isDeleted == ApiPairingDocSet.DELETED) {
|
||||
docSetMap.remove(docSet.id);
|
||||
|
||||
for (String appId : docSet.appIds) {
|
||||
Set<ApiPairingDocSet> dss = appDocSetMap.get(appId);
|
||||
if (dss != null) {
|
||||
dss.remove(docSet);
|
||||
if (dss.isEmpty()) {
|
||||
appDocSetMap.remove(appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ApiPairingDoc doc : docSet.docs) {
|
||||
Set<ApiPairingDocSet> dss = serviceExistsInDocSetMap.get(doc.service);
|
||||
if (dss != null) {
|
||||
dss.remove(docSet);
|
||||
if (dss.isEmpty()) {
|
||||
serviceExistsInDocSetMap.remove(doc.service);
|
||||
}
|
||||
}
|
||||
for (Api api : doc.apis) {
|
||||
Map<Object, Set<ApiPairingDocSet>> methodDocSetMap = pathMethodExistsInDocSetMap.get(api.path);
|
||||
if (methodDocSetMap != null) {
|
||||
dss = methodDocSetMap.get(api.method);
|
||||
if (dss != null) {
|
||||
dss.remove(docSet);
|
||||
if (dss.isEmpty()) {
|
||||
methodDocSetMap.remove(api.method);
|
||||
if (methodDocSetMap.isEmpty()) {
|
||||
pathMethodExistsInDocSetMap.remove(api.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("delete doc set: {}", docSet);
|
||||
|
||||
} else {
|
||||
docSetMap.put(docSet.id, docSet);
|
||||
docSet.appIds.forEach(
|
||||
appId -> {
|
||||
Set<ApiPairingDocSet> dss = appDocSetMap.get(appId);
|
||||
if (dss == null) {
|
||||
dss = new HashSet<>();
|
||||
appDocSetMap.put(appId, dss);
|
||||
}
|
||||
dss.add(docSet);
|
||||
}
|
||||
);
|
||||
docSet.docs.forEach(
|
||||
doc -> {
|
||||
Set<ApiPairingDocSet> dss = serviceExistsInDocSetMap.get(doc.service);
|
||||
if (dss == null) {
|
||||
dss = new HashSet<>();
|
||||
serviceExistsInDocSetMap.put(doc.service, dss);
|
||||
}
|
||||
dss.add(docSet);
|
||||
for (Api api : doc.apis) {
|
||||
Map<Object, Set<ApiPairingDocSet>> methodDocSetMap = pathMethodExistsInDocSetMap.get(api.path);
|
||||
if (methodDocSetMap == null) {
|
||||
methodDocSetMap = new HashMap<>(8);
|
||||
pathMethodExistsInDocSetMap.put(api.path, methodDocSetMap);
|
||||
}
|
||||
dss = methodDocSetMap.get(api.method);
|
||||
if (dss == null) {
|
||||
dss = new HashSet<>();
|
||||
methodDocSetMap.put(api.method, dss);
|
||||
}
|
||||
dss.add(docSet);
|
||||
}
|
||||
}
|
||||
);
|
||||
log.info("update doc set: {}", docSet);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Integer, ApiPairingDocSet> getDocSetMap() {
|
||||
return docSetMap;
|
||||
}
|
||||
|
||||
public ApiPairingDocSet get(int id) {
|
||||
return docSetMap.get(id);
|
||||
}
|
||||
|
||||
public Map<String, Set<ApiPairingDocSet>> getAppDocSetMap() {
|
||||
return appDocSetMap;
|
||||
}
|
||||
|
||||
public Map<String, Set<ApiPairingDocSet>> getServiceExistsInDocSetMap() {
|
||||
return serviceExistsInDocSetMap;
|
||||
}
|
||||
|
||||
public Map<String, Map<Object, Set<ApiPairingDocSet>>> getPathMethodExistsInDocSetMap() {
|
||||
return pathMethodExistsInDocSetMap;
|
||||
}
|
||||
|
||||
public boolean existsDocSetMatch(String appId, HttpMethod method, String service, String path) {
|
||||
Set<ApiPairingDocSet> appDocSets = appDocSetMap.get(appId);
|
||||
if (appDocSets == null) {
|
||||
return false;
|
||||
}
|
||||
Set<ApiPairingDocSet> serviceDocSets = serviceExistsInDocSetMap.get(service);
|
||||
if (serviceDocSets == null) {
|
||||
return false;
|
||||
}
|
||||
Map<Object, Set<ApiPairingDocSet>> methodDocSetMap = pathMethodExistsInDocSetMap.get(path);
|
||||
if (methodDocSetMap == null) {
|
||||
return false;
|
||||
}
|
||||
Set<ApiPairingDocSet> pathMethodDocSets = methodDocSetMap.get(method);
|
||||
if (pathMethodDocSets == null) {
|
||||
pathMethodDocSets = methodDocSetMap.get(ApiConfig.ALL_METHOD);
|
||||
if (pathMethodDocSets == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Set<ApiPairingDocSet> s = new HashSet<>(appDocSets);
|
||||
s.retainAll(serviceDocSets);
|
||||
if (s.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
s.retainAll(pathMethodDocSets);
|
||||
if (s.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
49
fizz-core/src/main/java/we/api/pairing/ApiPairingInfo.java
Normal file
49
fizz-core/src/main/java/we/api/pairing/ApiPairingInfo.java
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import we.util.JacksonUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class ApiPairingInfo {
|
||||
|
||||
public static final int DELETED = 1;
|
||||
|
||||
public int isDeleted = 0;
|
||||
|
||||
public String id; // uuid
|
||||
|
||||
public String url;
|
||||
|
||||
public String appId;
|
||||
|
||||
public String secretKey;
|
||||
|
||||
public List<String> services = Collections.emptyList();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JacksonUtils.writeValueAsString(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.api.pairing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import we.Fizz;
|
||||
import we.config.AggregateRedisConfig;
|
||||
import we.util.JacksonUtils;
|
||||
import we.util.ReactiveResult;
|
||||
import we.util.Result;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
@Service
|
||||
public class ApiPairingInfoService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiPairingInfoService.class);
|
||||
|
||||
private Map<String , ApiPairingInfo> serviceApiPairingInfoMap = new HashMap<>(64);
|
||||
|
||||
@Resource(name = AggregateRedisConfig.AGGREGATE_REACTIVE_REDIS_TEMPLATE)
|
||||
private ReactiveStringRedisTemplate rt;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws Throwable {
|
||||
Result<?> result = initApiPairingInfo();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
result = lsnApiPairingInfoChange();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
}
|
||||
|
||||
private Result<?> initApiPairingInfo() {
|
||||
Result<?> result = Result.succ();
|
||||
Flux<Map.Entry<Object, Object>> resources = rt.opsForHash().entries("fizz_api_pairing_info");
|
||||
resources.collectList()
|
||||
.defaultIfEmpty(Collections.emptyList())
|
||||
.flatMap(
|
||||
es -> {
|
||||
if (Fizz.context != null) {
|
||||
String json = null;
|
||||
try {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
json = (String) e.getValue();
|
||||
ApiPairingInfo info = JacksonUtils.readValue(json, ApiPairingInfo.class);
|
||||
for (String service : info.services) {
|
||||
serviceApiPairingInfoMap.put(service, info);
|
||||
}
|
||||
log.info("init api pairing info: {}", info);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init api pairing info error, info: " + json;
|
||||
result.t = t;
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
)
|
||||
.onErrorReturn(
|
||||
throwable -> {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init api pairing info error";
|
||||
result.t = throwable;
|
||||
return true;
|
||||
},
|
||||
result
|
||||
)
|
||||
.block();
|
||||
return result;
|
||||
}
|
||||
|
||||
private Result<?> lsnApiPairingInfoChange() {
|
||||
Result<?> result = Result.succ();
|
||||
String channel = "fizz_api_pairing_info_channel";
|
||||
rt.listenToChannel(channel)
|
||||
.doOnError(
|
||||
t -> {
|
||||
result.code = ReactiveResult.FAIL;
|
||||
result.msg = "lsn error, channel: " + channel;
|
||||
result.t = t;
|
||||
log.error("lsn channel {} error", channel, t);
|
||||
}
|
||||
)
|
||||
.doOnSubscribe(
|
||||
s -> {
|
||||
log.info("success to lsn on {}", channel);
|
||||
}
|
||||
)
|
||||
.doOnNext(
|
||||
msg -> {
|
||||
if (Fizz.context != null) {
|
||||
String message = msg.getMessage();
|
||||
try {
|
||||
ApiPairingInfo info = JacksonUtils.readValue(message, ApiPairingInfo.class);
|
||||
if (info.isDeleted == ApiPairingDocSet.DELETED) {
|
||||
for (String service : info.services) {
|
||||
serviceApiPairingInfoMap.remove(service);
|
||||
}
|
||||
log.info("remove api pairing info: {}", info);
|
||||
} else {
|
||||
for (String service : info.services) {
|
||||
serviceApiPairingInfoMap.put(service, info);
|
||||
}
|
||||
log.info("update api pairing info: {}", info);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log.error("update api pairing info error, {}", message, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, ApiPairingInfo> getServiceApiPairingInfoMap() {
|
||||
return serviceApiPairingInfoMap;
|
||||
}
|
||||
|
||||
public ApiPairingInfo get(String service) {
|
||||
return serviceApiPairingInfoMap.get(service);
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import we.util.ThreadContext;
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public abstract class PairingUtils extends org.apache.commons.codec.digest.DigestUtils {
|
||||
public abstract class ApiPairingUtils extends org.apache.commons.codec.digest.DigestUtils {
|
||||
|
||||
private PairingUtils() {
|
||||
private ApiPairingUtils() {
|
||||
}
|
||||
|
||||
public static String sign(String app, String timestamp, String secretKey) {
|
||||
@@ -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.api.pairing;
|
||||
|
||||
import we.util.JacksonUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class AppApiPairingDocSet {
|
||||
|
||||
public int id; // 文档集 id
|
||||
|
||||
public String name;
|
||||
|
||||
public Set<String> services;
|
||||
|
||||
public boolean enabled;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return JacksonUtils.writeValueAsString(this);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package we.api.pairing;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -26,20 +27,22 @@ import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
import org.springframework.web.server.i18n.LocaleContextResolver;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
import reactor.core.publisher.Mono;
|
||||
import we.Fizz;
|
||||
import we.plugin.auth.ApiConfigService;
|
||||
import we.spring.web.server.ext.FizzServerWebExchangeDecorator;
|
||||
import we.util.JacksonUtils;
|
||||
import we.config.SystemConfig;
|
||||
import we.proxy.FizzWebClient;
|
||||
import we.util.Consts;
|
||||
import we.util.ThreadContext;
|
||||
import we.util.WebUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -48,7 +51,7 @@ import java.util.Set;
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
public class FizzApiPairingHttpHandler implements HttpHandler {
|
||||
class FizzApiPairingHttpHandler implements HttpHandler {
|
||||
|
||||
private static final String disconnected_client_log_category = "DisconnectedClient";
|
||||
|
||||
@@ -64,12 +67,21 @@ public class FizzApiPairingHttpHandler implements HttpHandler {
|
||||
private ForwardedHeaderTransformer forwardedHeaderTransformer;
|
||||
private boolean enableLoggingRequestDetails = false;
|
||||
|
||||
public FizzApiPairingHttpHandler(WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer,
|
||||
private SystemConfig systemConfig;
|
||||
private FizzWebClient fizzWebClient;
|
||||
private ApiPairingInfoService apiPairingInfoService;
|
||||
|
||||
public FizzApiPairingHttpHandler(ConfigurableApplicationContext applicationContext, WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer,
|
||||
LocaleContextResolver localeContextResolver, ForwardedHeaderTransformer forwardedHeaderTransformer) {
|
||||
|
||||
this.sessionManager = sessionManager;
|
||||
this.serverCodecConfigurer = codecConfigurer;
|
||||
this.localeContextResolver = localeContextResolver;
|
||||
this.forwardedHeaderTransformer = forwardedHeaderTransformer;
|
||||
|
||||
systemConfig = applicationContext.getBean(SystemConfig.class);
|
||||
fizzWebClient = applicationContext.getBean(FizzWebClient.class);
|
||||
apiPairingInfoService = applicationContext.getBean(ApiPairingInfoService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,38 +99,81 @@ public class FizzApiPairingHttpHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
DefaultServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager, serverCodecConfigurer, localeContextResolver);
|
||||
String logPrefix = exchange.getLogPrefix();
|
||||
|
||||
URI requestURI = request.getURI();
|
||||
String path = requestURI.getPath();
|
||||
int secFS = path.indexOf(Consts.S.FORWARD_SLASH, 1);
|
||||
String service = path.substring(1, secFS);
|
||||
ApiPairingInfo apiPairingInfo = apiPairingInfoService.get(service);
|
||||
if (apiPairingInfo == null) {
|
||||
log.warn("{}{} 服务无配对信息", logPrefix, service);
|
||||
return WebUtils.buildDirectResponse(response, HttpStatus.FORBIDDEN, null, service + " 服务无配对信息").then(response.setComplete());
|
||||
}
|
||||
|
||||
StringBuilder b = ThreadContext.getStringBuilder();
|
||||
b.append(apiPairingInfo.url).append(path);
|
||||
String rawQuery = requestURI.getRawQuery();
|
||||
if (StringUtils.hasText(rawQuery)) {
|
||||
b.append(Consts.S.QUESTION).append(rawQuery);
|
||||
}
|
||||
String targetUrl = b.toString();
|
||||
String appId = apiPairingInfo.appId;
|
||||
String secretKey = apiPairingInfo.secretKey;
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String sign = ApiPairingUtils.sign(appId, timestamp, secretKey);
|
||||
|
||||
HttpHeaders writableHttpHeaders = HttpHeaders.writableHttpHeaders(request.getHeaders());
|
||||
writableHttpHeaders.set(SystemConfig.FIZZ_APP_ID, appId);
|
||||
writableHttpHeaders.set(SystemConfig.FIZZ_TIMESTAMP, timestamp);
|
||||
writableHttpHeaders.set(SystemConfig.FIZZ_SIGN, sign);
|
||||
|
||||
int requestTimeout = systemConfig.fizzApiPairingRequestTimeout();
|
||||
int retryCount = systemConfig.fizzApiPairingRequestRetryCount();
|
||||
int retryInterval = systemConfig.fizzApiPairingRequestRetryInterval();
|
||||
|
||||
// XXX
|
||||
String clientReqPath = WebUtils.getClientReqPath(exchange);
|
||||
log.info("client request path: {}", clientReqPath);
|
||||
Mono<MultiValueMap<String, String>> formData = exchange.getFormData().defaultIfEmpty(FizzServerWebExchangeDecorator.EMPTY_FORM_DATA).flatMap(
|
||||
dat -> {
|
||||
log.info("form data: {}", JacksonUtils.writeValueAsString(dat));
|
||||
return Mono.just(dat);
|
||||
}
|
||||
);
|
||||
try {
|
||||
ApiConfigService apiConfigService = Fizz.context.getBean(ApiConfigService.class);
|
||||
String apiConfigs = JacksonUtils.writeValueAsString(apiConfigService.serviceConfigMap);
|
||||
return
|
||||
formData.then(
|
||||
response.writeWith(Mono.just(response.bufferFactory().wrap(apiConfigs.getBytes())))
|
||||
)
|
||||
.doOnSuccess ( v -> logResponse(exchange) )
|
||||
.onErrorResume( t -> handleUnresolvedError(exchange, t) )
|
||||
.then ( Mono.defer(response::setComplete) );
|
||||
Mono<ClientResponse> remoteResponseMono = fizzWebClient.send( request.getId(), request.getMethod(), targetUrl, writableHttpHeaders, request.getBody(),
|
||||
requestTimeout, retryCount, retryInterval );
|
||||
|
||||
Mono<Void> respMono = remoteResponseMono.flatMap(
|
||||
remoteResp -> {
|
||||
response.setStatusCode(remoteResp.statusCode());
|
||||
HttpHeaders respHeaders = response.getHeaders();
|
||||
HttpHeaders remoteRespHeaders = remoteResp.headers().asHttpHeaders();
|
||||
respHeaders.putAll(remoteRespHeaders);
|
||||
if (log.isDebugEnabled()) {
|
||||
StringBuilder sb = ThreadContext.getStringBuilder();
|
||||
WebUtils.response2stringBuilder(logPrefix, remoteResp, sb);
|
||||
log.debug(sb.toString());
|
||||
}
|
||||
return response.writeWith ( remoteResp.body(BodyExtractors.toDataBuffers()) )
|
||||
.doOnError ( throwable -> cleanup(remoteResp) )
|
||||
.doOnCancel( () -> cleanup(remoteResp) );
|
||||
}
|
||||
);
|
||||
|
||||
return respMono.doOnSuccess ( v -> logResponse(exchange) )
|
||||
.onErrorResume( t -> handleUnresolvedError(exchange, t) )
|
||||
.then ( Mono.defer(response::setComplete) );
|
||||
|
||||
} catch (Throwable t) {
|
||||
// throw t;
|
||||
log.error(exchange.getLogPrefix() + " 500 Server Error for " + formatRequest(request), t);
|
||||
log.error(logPrefix + "500 Server Error for " + formatRequest(request), t);
|
||||
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
return response.setComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup(ClientResponse clientResponse) {
|
||||
if (clientResponse != null) {
|
||||
clientResponse.bodyToMono(Void.class).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private void logResponse(ServerWebExchange exchange) {
|
||||
WebUtils.traceDebug(log, traceOn -> {
|
||||
HttpStatus status = exchange.getResponse().getStatusCode();
|
||||
return exchange.getLogPrefix() + " Completed " + (status != null ? status : "200 OK") + (traceOn ? ", headers=" + formatHeaders(exchange.getResponse().getHeaders()) : "");
|
||||
return exchange.getLogPrefix() + "Completed " + (status != null ? status : "200 OK") + (traceOn ? ", headers=" + formatHeaders(exchange.getResponse().getHeaders()) : "");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,20 +194,20 @@ public class FizzApiPairingHttpHandler implements HttpHandler {
|
||||
String logPrefix = exchange.getLogPrefix();
|
||||
|
||||
if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) {
|
||||
log.error(logPrefix + " 500 Server Error for " + formatRequest(request), t);
|
||||
log.error(logPrefix + "500 Server Error for " + formatRequest(request), t);
|
||||
return Mono.empty();
|
||||
|
||||
} else if (isDisconnectedClientError(t)) {
|
||||
if (lostClientLog.isTraceEnabled()) {
|
||||
lostClientLog.trace(logPrefix + " Client went away", t);
|
||||
lostClientLog.trace(logPrefix + "Client went away", t);
|
||||
} else if (lostClientLog.isDebugEnabled()) {
|
||||
lostClientLog.debug(logPrefix + " Client went away: " + t + " (stacktrace at TRACE level for '" + disconnected_client_log_category + "')");
|
||||
lostClientLog.debug(logPrefix + "Client went away: " + t + " (stacktrace at TRACE level for '" + disconnected_client_log_category + "')");
|
||||
}
|
||||
return Mono.empty();
|
||||
|
||||
} else {
|
||||
// After the response is committed, propagate errors to the server...
|
||||
log.error(logPrefix + " Error [" + t + "] for " + formatRequest(request) + ", but ServerHttpResponse already committed (" + response.getStatusCode() + ")");
|
||||
log.error(logPrefix + "Error [" + t + "] for " + formatRequest(request) + ", but ServerHttpResponse already committed (" + response.getStatusCode() + ")");
|
||||
return Mono.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
|
||||
@@ -46,9 +47,12 @@ public class FizzApiPairingWebServer {
|
||||
private static final Logger log = LoggerFactory.getLogger(FizzApiPairingWebServer.class);
|
||||
|
||||
@Resource
|
||||
private HttpHandler httpHandler;
|
||||
private ConfigurableApplicationContext applicationContext;
|
||||
|
||||
private WebServer server;
|
||||
@Resource
|
||||
private HttpHandler httpHandler;
|
||||
|
||||
private WebServer server;
|
||||
|
||||
@Value("${fizz.api.pairing.web-server.port:8601}")
|
||||
private int port = 8601;
|
||||
@@ -59,6 +63,7 @@ public class FizzApiPairingWebServer {
|
||||
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory(port);
|
||||
server = factory.getWebServer(
|
||||
new FizzApiPairingHttpHandler(
|
||||
applicationContext,
|
||||
new DefaultWebSessionManager(),
|
||||
adapter.getCodecConfigurer(),
|
||||
adapter.getLocaleContextResolver(),
|
||||
|
||||
@@ -28,11 +28,7 @@ import we.util.Consts;
|
||||
import we.util.WebUtils;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -47,26 +43,23 @@ public class SystemConfig {
|
||||
private static final Logger log = LoggerFactory.getLogger(SystemConfig.class);
|
||||
|
||||
public static final String DEFAULT_GATEWAY_PREFIX = "/proxy";
|
||||
|
||||
public static final String DEFAULT_GATEWAY_TEST_PREFIX = "/_proxytest";
|
||||
|
||||
public static final String DEFAULT_GATEWAY_TEST = "_proxytest";
|
||||
|
||||
public static final String DEFAULT_GATEWAY_TEST_PREFIX0 = "/_proxytest/";
|
||||
|
||||
public static boolean FIZZ_ERR_RESP_HTTP_STATUS_ENABLE = true;
|
||||
|
||||
public static String FIZZ_ERR_RESP_CODE_FIELD = "msgCode";
|
||||
|
||||
public static String FIZZ_ERR_RESP_MSG_FIELD = "message";
|
||||
|
||||
public static final String FIZZ_APP_ID = "fizz-appid";
|
||||
public static final String FIZZ_SIGN = "fizz-sign";
|
||||
public static final String FIZZ_TIMESTAMP = "fizz-ts";
|
||||
|
||||
private String gatewayPrefix = DEFAULT_GATEWAY_PREFIX;
|
||||
|
||||
private List<String> appHeaders = Stream.of("fizz-appid").collect(Collectors.toList());
|
||||
|
||||
private List<String> signHeaders = Stream.of("fizz-sign") .collect(Collectors.toList());
|
||||
|
||||
private List<String> timestampHeaders = Stream.of("fizz-ts") .collect(Collectors.toList());
|
||||
private List<String> appHeaders = Stream.of(FIZZ_APP_ID) .collect(Collectors.toList());
|
||||
private List<String> signHeaders = Stream.of(FIZZ_SIGN) .collect(Collectors.toList());
|
||||
private List<String> timestampHeaders = Stream.of(FIZZ_TIMESTAMP).collect(Collectors.toList());
|
||||
|
||||
private List<String> proxySetHeaders = new ArrayList<>();
|
||||
|
||||
@@ -99,6 +92,38 @@ public class SystemConfig {
|
||||
FIZZ_ERR_RESP_MSG_FIELD = fizzErrRespMsgField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Value("${fizz.api.pairing.request.timeliness:300}")
|
||||
private int fizzApiPairingRequestTimeliness = 300; // unit: sec
|
||||
|
||||
@Value("${fizz.api.pairing.request.timeout:0}")
|
||||
private int fizzApiPairingRequestTimeout = 0; // mills
|
||||
|
||||
@Value("${fizz.api.pairing.request.retry-count:0}")
|
||||
private int fizzApiPairingRequestRetryCount = 0;
|
||||
|
||||
@Value("${fizz.api.pairing.request.retry-interval:0}")
|
||||
private int fizzApiPairingRequestRetryInterval = 0; // mills
|
||||
|
||||
public int fizzApiPairingRequestTimeout() {
|
||||
return fizzApiPairingRequestTimeout;
|
||||
}
|
||||
|
||||
public int fizzApiPairingRequestRetryCount() {
|
||||
return fizzApiPairingRequestRetryCount;
|
||||
}
|
||||
|
||||
public int fizzApiPairingRequestRetryInterval() {
|
||||
return fizzApiPairingRequestRetryInterval;
|
||||
}
|
||||
|
||||
public int fizzApiPairingRequestTimeliness() {
|
||||
return fizzApiPairingRequestTimeliness;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String fizzTraceIdHeader() {
|
||||
return fizzTraceIdHeader;
|
||||
}
|
||||
@@ -200,8 +225,12 @@ public class SystemConfig {
|
||||
return aggregateTestAuth;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: below to X
|
||||
|
||||
|
||||
|
||||
private boolean logResponseBody;
|
||||
|
||||
private String logHeaders;
|
||||
|
||||
@@ -187,9 +187,9 @@ public class RouteFilter extends FizzWebFilter {
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
StringBuilder b = ThreadContext.getStringBuilder();
|
||||
String rid = WebUtils.getTraceId(exchange);
|
||||
WebUtils.response2stringBuilder(rid, remoteResp, b);
|
||||
log.debug(b.toString(), LogService.BIZ_ID, rid);
|
||||
String traceId = WebUtils.getTraceId(exchange);
|
||||
WebUtils.response2stringBuilder(traceId, remoteResp, b);
|
||||
log.debug(b.toString(), LogService.BIZ_ID, traceId);
|
||||
}
|
||||
return clientResp.writeWith(remoteResp.body(BodyExtractors.toDataBuffers()))
|
||||
.doOnError(throwable -> cleanup(remoteResp)).doOnCancel(() -> cleanup(remoteResp));
|
||||
|
||||
@@ -26,6 +26,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import we.Fizz;
|
||||
import we.config.AggregateRedisConfig;
|
||||
import we.config.SystemConfig;
|
||||
import we.fizz.input.PathMapping;
|
||||
import we.util.JacksonUtils;
|
||||
import we.util.ReactiveResult;
|
||||
@@ -58,30 +59,15 @@ public class GlobalResourceService {
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws Throwable {
|
||||
initGlobalResource().subscribe(
|
||||
r -> {
|
||||
if (r.code == ReactiveResult.SUCC) {
|
||||
lsnGlobalResourceChange().subscribe(
|
||||
res -> {
|
||||
if (res.code == ReactiveResult.FAIL) {
|
||||
log.error(res.toString());
|
||||
if (res.t == null) {
|
||||
throw Utils.runtimeExceptionWithoutStack("lsn global resource error");
|
||||
}
|
||||
throw new RuntimeException(res.t);
|
||||
}
|
||||
updateResNode();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
log.error(r.toString());
|
||||
if (r.t == null) {
|
||||
throw Utils.runtimeExceptionWithoutStack("init global resource error");
|
||||
}
|
||||
throw new RuntimeException(r.t);
|
||||
}
|
||||
}
|
||||
);
|
||||
Result<?> result = initGlobalResource();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
result = lsnGlobalResourceChange();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
updateResNode();
|
||||
}
|
||||
|
||||
private void updateResNode() {
|
||||
@@ -89,42 +75,55 @@ public class GlobalResourceService {
|
||||
log.info("global resource node is updated, new keys: {}", objectMap.keySet());
|
||||
}
|
||||
|
||||
private Mono<Result<?>> initGlobalResource() {
|
||||
private Result<?> initGlobalResource() {
|
||||
Result<?> result = Result.succ();
|
||||
Flux<Map.Entry<Object, Object>> resources = rt.opsForHash().entries("fizz_global_resource");
|
||||
resources.collectList()
|
||||
.defaultIfEmpty(Collections.emptyList())
|
||||
.flatMap(
|
||||
es -> {
|
||||
if (Fizz.context != null) {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
String json = (String) e.getValue();
|
||||
GlobalResource r = JacksonUtils.readValue(json, GlobalResource.class);
|
||||
resourceMap.put(r.key, r);
|
||||
objectMap.put(r.key, r.originalVal);
|
||||
log.info("init global resource {}", r.key);
|
||||
String json = null;
|
||||
try {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
json = (String) e.getValue();
|
||||
GlobalResource r = JacksonUtils.readValue(json, GlobalResource.class);
|
||||
resourceMap.put(r.key, r);
|
||||
objectMap.put(r.key, r.originalVal);
|
||||
log.info("init global resource {}", r.key);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init global resource error, json: " + json;
|
||||
result.t = t;
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
)
|
||||
.doOnError(
|
||||
t -> {
|
||||
log.error("init global resource", t);
|
||||
}
|
||||
.onErrorReturn(
|
||||
throwable -> {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init global resource error";
|
||||
result.t = throwable;
|
||||
return true;
|
||||
},
|
||||
result
|
||||
)
|
||||
.block();
|
||||
return Mono.just(Result.succ());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Mono<Result<?>> lsnGlobalResourceChange() {
|
||||
private Result<?> lsnGlobalResourceChange() {
|
||||
Result<?> result = Result.succ();
|
||||
String channel = "fizz_global_resource_channel";
|
||||
rt.listenToChannel(channel)
|
||||
.doOnError(
|
||||
t -> {
|
||||
result.code = ReactiveResult.FAIL;
|
||||
result.t = t;
|
||||
log.error("lsn {}", channel, t);
|
||||
result.msg = "lsn error, channel: " + channel;
|
||||
result.t = t;
|
||||
log.error("lsn channel {} error", channel, t);
|
||||
}
|
||||
)
|
||||
.doOnSubscribe(
|
||||
@@ -155,7 +154,7 @@ public class GlobalResourceService {
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
return Mono.just(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, GlobalResource> getResourceMap() {
|
||||
|
||||
@@ -86,29 +86,14 @@ public class ApiConfigService {
|
||||
@PostConstruct
|
||||
public void init() throws Throwable {
|
||||
this.init(this::lsnApiConfigChange);
|
||||
initPlugin().subscribe(
|
||||
r -> {
|
||||
if (r.code == ReactiveResult.SUCC) {
|
||||
lsnPluginConfigChange().subscribe(
|
||||
res -> {
|
||||
if (res.code == ReactiveResult.FAIL) {
|
||||
log.error(res.toString());
|
||||
if (res.t == null) {
|
||||
throw Utils.runtimeExceptionWithoutStack("lsn plugin config error");
|
||||
}
|
||||
throw new RuntimeException(res.t);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
log.error(r.toString());
|
||||
if (r.t == null) {
|
||||
throw Utils.runtimeExceptionWithoutStack("init plugin error");
|
||||
}
|
||||
throw new RuntimeException(r.t);
|
||||
}
|
||||
}
|
||||
);
|
||||
Result<?> result = initPlugin();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
result = lsnPluginConfigChange();
|
||||
if (result.code == Result.FAIL) {
|
||||
throw new RuntimeException(result.msg, result.t);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: no need like this
|
||||
@@ -210,7 +195,8 @@ public class ApiConfigService {
|
||||
return Mono.just(ReactorUtils.EMPTY_THROWABLE);
|
||||
}
|
||||
|
||||
private Mono<ReactiveResult<?>> initPlugin() {
|
||||
private Result<?> initPlugin() {
|
||||
Result<?> result = Result.succ();
|
||||
String key = apiConfigServiceProperties.getFizzPluginConfig();
|
||||
Flux<Map.Entry<Object, Object>> plugins = rt.opsForHash().entries(key);
|
||||
plugins.collectList()
|
||||
@@ -218,45 +204,57 @@ public class ApiConfigService {
|
||||
.flatMap(
|
||||
es -> {
|
||||
if (Fizz.context != null) {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
String json = (String) e.getValue();
|
||||
HashMap<?, ?> map = JacksonUtils.readValue(json, HashMap.class);
|
||||
String plugin = (String) map.get("plugin");
|
||||
String pluginConfig = (String) map.get("fixedConfig");
|
||||
String currentPluginConfig = pluginConfigMap.get(plugin);
|
||||
if (currentPluginConfig == null || !currentPluginConfig.equals(pluginConfig)) {
|
||||
if (Fizz.context.containsBean(plugin)) {
|
||||
FizzPluginFilter pluginFilter = (FizzPluginFilter) Fizz.context.getBean(plugin);
|
||||
pluginFilter.init(pluginConfig);
|
||||
pluginConfigMap.put(plugin, pluginConfig);
|
||||
log.info("init {} with {}", plugin, pluginConfig);
|
||||
} else {
|
||||
log.warn("no {} bean", plugin);
|
||||
String json = null;
|
||||
try {
|
||||
for (Map.Entry<Object, Object> e : es) {
|
||||
json = (String) e.getValue();
|
||||
HashMap<?, ?> map = JacksonUtils.readValue(json, HashMap.class);
|
||||
String plugin = (String) map.get("plugin");
|
||||
String pluginConfig = (String) map.get("fixedConfig");
|
||||
String currentPluginConfig = pluginConfigMap.get(plugin);
|
||||
if (currentPluginConfig == null || !currentPluginConfig.equals(pluginConfig)) {
|
||||
if (Fizz.context.containsBean(plugin)) {
|
||||
FizzPluginFilter pluginFilter = (FizzPluginFilter) Fizz.context.getBean(plugin);
|
||||
pluginFilter.init(pluginConfig);
|
||||
pluginConfigMap.put(plugin, pluginConfig);
|
||||
log.info("init {} with {}", plugin, pluginConfig);
|
||||
} else {
|
||||
log.warn("no {} bean", plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init plugin error, config: " + json;
|
||||
result.t = t;
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
)
|
||||
.doOnError(
|
||||
t -> {
|
||||
log.error("init plugin", t);
|
||||
}
|
||||
.onErrorReturn(
|
||||
throwable -> {
|
||||
result.code = Result.FAIL;
|
||||
result.msg = "init plugin error";
|
||||
result.t = throwable;
|
||||
return true;
|
||||
},
|
||||
result
|
||||
)
|
||||
.block();
|
||||
return Mono.just(ReactiveResult.succ());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Mono<ReactiveResult<?>> lsnPluginConfigChange() {
|
||||
ReactiveResult<?> result = ReactiveResult.succ();
|
||||
private Result<?> lsnPluginConfigChange() {
|
||||
Result<?> result = Result.succ();
|
||||
String channel = apiConfigServiceProperties.getFizzPluginConfigChannel();
|
||||
rt.listenToChannel(channel)
|
||||
.doOnError(
|
||||
t -> {
|
||||
result.code = ReactiveResult.FAIL;
|
||||
result.t = t;
|
||||
log.error("lsn {}", channel, t);
|
||||
result.msg = "lsn error, channel: " + channel;
|
||||
result.t = t;
|
||||
log.error("lsn channel {} error", channel, t);
|
||||
}
|
||||
)
|
||||
.doOnSubscribe(
|
||||
@@ -290,7 +288,7 @@ public class ApiConfigService {
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
return Mono.just(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updateServiceConfigMap(ApiConfig ac, Map<String, ServiceConfig> serviceConfigMap) {
|
||||
@@ -316,6 +314,7 @@ public class ApiConfigService {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public enum Access {
|
||||
|
||||
YES (null),
|
||||
|
||||
@@ -196,9 +196,9 @@ public class CallbackService {
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
StringBuilder b = ThreadContext.getStringBuilder();
|
||||
String rid = WebUtils.getTraceId(exchange);
|
||||
WebUtils.response2stringBuilder(rid, remoteResp, b);
|
||||
log.debug(b.toString(), LogService.BIZ_ID, rid);
|
||||
String traceId = WebUtils.getTraceId(exchange);
|
||||
WebUtils.response2stringBuilder(traceId, remoteResp, b);
|
||||
log.debug(b.toString(), LogService.BIZ_ID, traceId);
|
||||
}
|
||||
return clientResp.writeWith(remoteResp.body(BodyExtractors.toDataBuffers()))
|
||||
.doOnError(throwable -> clean(remoteResp)).doOnCancel(() -> clean(remoteResp));
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package we.api.pairing;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import we.Fizz;
|
||||
import we.redis.RedisProperties;
|
||||
import we.redis.RedisServerConfiguration;
|
||||
import we.redis.RedisTemplateConfiguration;
|
||||
import we.util.ReflectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author hongqiaowei
|
||||
*/
|
||||
|
||||
@TestPropertySource("/application.properties")
|
||||
@SpringJUnitConfig(classes = {RedisProperties.class, RedisTemplateConfiguration.class, RedisServerConfiguration.class})
|
||||
public class ApiPairingDocSetServiceTests {
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
ReactiveStringRedisTemplate reactiveStringRedisTemplate;
|
||||
|
||||
ApiPairingDocSetService apiPairingDocSetService;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws NoSuchFieldException {
|
||||
apiPairingDocSetService = new ApiPairingDocSetService();
|
||||
ReflectionUtils.set(apiPairingDocSetService, "rt", reactiveStringRedisTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void initTest() throws Throwable {
|
||||
|
||||
Fizz.context = new GenericApplicationContext();
|
||||
Fizz.context.refresh();
|
||||
|
||||
Map<String, String> hash = new HashMap<>();
|
||||
hash.put("c", "{\"id\":1,\"name\":\"DocSet1\",\"docs\":[{\"service\":\"we-meb\",\"apis\":[{\"method\":\"GET\",\"path\":\"/getMebInfo\"}]}],\"appIds\":[\"app1\"]}");
|
||||
hash.put("d", "{\"id\":2,\"name\":\"DocSet2\",\"docs\":[{\"service\":\"we-meb\",\"apis\":[{\"method\":\"GET\",\"path\":\"/getMebInfo\"}]}],\"appIds\":[\"app1\"]}");
|
||||
// hash.put("a", "{\"isDeleted\":1,\"id\":1,\"name\":\"DocSet1\",\"docs\":[{\"service\":\"we-meb\",\"apis\":[{\"method\":\"GET\",\"path\":\"/getMebInfo\"}]}],\"appIds\":[\"app1\"]}");
|
||||
// hash.put("b", "{\"isDeleted\":1,\"id\":2,\"name\":\"DocSet2\",\"docs\":[{\"service\":\"we-meb\",\"apis\":[{\"method\":\"GET\",\"path\":\"/getMebInfo\"}]}],\"appIds\":[\"app1\"]}");
|
||||
stringRedisTemplate.opsForHash().putAll("fizz_api_pairing_doc", hash);
|
||||
|
||||
apiPairingDocSetService.init();
|
||||
Map<Integer, ApiPairingDocSet> docSetMap = apiPairingDocSetService.getDocSetMap();
|
||||
Map<String, Set<ApiPairingDocSet>> appDocSetMap = apiPairingDocSetService.getAppDocSetMap();
|
||||
Map<String, Set<ApiPairingDocSet>> serviceExistsInDocSetMap = apiPairingDocSetService.getServiceExistsInDocSetMap();
|
||||
Map<String, Map<Object, Set<ApiPairingDocSet>>> pathMethodExistsInDocSetMap = apiPairingDocSetService.getPathMethodExistsInDocSetMap();
|
||||
// System.err.println("docSetMap: " + JacksonUtils.writeValueAsString(docSetMap));
|
||||
// System.err.println("appDocSetMap: " + JacksonUtils.writeValueAsString(appDocSetMap));
|
||||
// System.err.println("serviceExistsInDocSetMap: " + JacksonUtils.writeValueAsString(serviceExistsInDocSetMap));
|
||||
// System.err.println("pathMethodExistsInDocSetMap: " + JacksonUtils.writeValueAsString(pathMethodExistsInDocSetMap));
|
||||
|
||||
boolean b = apiPairingDocSetService.existsDocSetMatch("app1", HttpMethod.GET, "we-meb", "/getMebInfo");
|
||||
Assert.assertTrue(b);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user