Merge branch 'eureka' into fit-eureka

pull/1/head
tanghc 5 years ago
commit 68e80ba984
  1. 1
      changelog.md
  2. 26
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java
  3. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/NacosEventProcessor.java
  4. 68
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRegistryListener.java
  5. 154
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRoutesListener.java
  6. 41
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseServiceListener.java
  7. 26
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java
  8. 71
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRegistryListener.java
  9. 75
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRoutesListener.java
  10. 12
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceListener.java
  11. 92
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceRouteListener.java
  12. 38
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignConfig.java
  13. 9
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java
  14. 5
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java
  15. 24
      sop-website/pom.xml
  16. 44
      sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java
  17. 50
      sop-website/src/main/java/com/gitee/sop/websiteserver/listener/ServiceDocListener.java
  18. 116
      sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocDiscovery.java
  19. 14
      sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManagerImpl.java
  20. 10
      sop-website/src/main/resources/application-dev.properties

@ -3,6 +3,7 @@
## 2.2.0 ## 2.2.0
- 支持eureka注册中心,需要执行`sop-2.2.0.sql`升级文件 - 支持eureka注册中心,需要执行`sop-2.2.0.sql`升级文件
- 签名内容支持urlencode(设置`sign.urlencode=true`)
## 2.1.3 ## 2.1.3

@ -8,15 +8,17 @@ import com.gitee.sop.gatewaycommon.limit.LimitManager;
import com.gitee.sop.gatewaycommon.loadbalancer.SopPropertiesFactory; import com.gitee.sop.gatewaycommon.loadbalancer.SopPropertiesFactory;
import com.gitee.sop.gatewaycommon.message.ErrorFactory; import com.gitee.sop.gatewaycommon.message.ErrorFactory;
import com.gitee.sop.gatewaycommon.param.ParameterFormatter; import com.gitee.sop.gatewaycommon.param.ParameterFormatter;
import com.gitee.sop.gatewaycommon.route.EurekaRoutesListener; import com.gitee.sop.gatewaycommon.route.ServiceRouteListener;
import com.gitee.sop.gatewaycommon.route.NacosRoutesListener; import com.gitee.sop.gatewaycommon.route.EurekaRegistryListener;
import com.gitee.sop.gatewaycommon.route.NacosRegistryListener;
import com.gitee.sop.gatewaycommon.route.RegistryListener; import com.gitee.sop.gatewaycommon.route.RegistryListener;
import com.gitee.sop.gatewaycommon.route.ServiceListener;
import com.gitee.sop.gatewaycommon.secret.IsvManager; import com.gitee.sop.gatewaycommon.secret.IsvManager;
import com.gitee.sop.gatewaycommon.session.SessionManager; import com.gitee.sop.gatewaycommon.session.SessionManager;
import com.gitee.sop.gatewaycommon.validate.SignConfig;
import com.gitee.sop.gatewaycommon.validate.Validator; import com.gitee.sop.gatewaycommon.validate.Validator;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
@ -72,13 +74,19 @@ public class AbstractConfiguration implements ApplicationContextAware {
@Bean @Bean
@ConditionalOnProperty("spring.cloud.nacos.discovery.server-addr") @ConditionalOnProperty("spring.cloud.nacos.discovery.server-addr")
RegistryListener registryListenerNacos() { RegistryListener registryListenerNacos() {
return new NacosRoutesListener(); return new NacosRegistryListener();
} }
@Bean @Bean
@ConditionalOnProperty("eureka.client.serviceUrl.defaultZone") @ConditionalOnProperty("eureka.client.serviceUrl.defaultZone")
RegistryListener registryListenerEureka() { RegistryListener registryListenerEureka() {
return new EurekaRoutesListener(); return new EurekaRegistryListener();
}
@Bean
@ConditionalOnMissingBean
ServiceListener serviceListener() {
return new ServiceRouteListener();
} }
@Bean @Bean
@ -174,6 +182,14 @@ public class AbstractConfiguration implements ApplicationContextAware {
if (RouteRepositoryContext.getRouteRepository() == null) { if (RouteRepositoryContext.getRouteRepository() == null) {
throw new IllegalArgumentException("RouteRepositoryContext.setRouteRepository()方法未使用"); throw new IllegalArgumentException("RouteRepositoryContext.setRouteRepository()方法未使用");
} }
String serverName = environment.getProperty("spring.application.name");
if (!"api-gateway".equals(serverName)) {
throw new IllegalArgumentException("spring.application.name必须为api-gateway");
}
String urlencode = environment.getProperty("sign.urlencode");
if ("true".equals(urlencode)) {
SignConfig.enableUrlencodeMode();
}
EnvironmentContext.setEnvironment(environment); EnvironmentContext.setEnvironment(environment);
initMessage(); initMessage();

@ -14,7 +14,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
/** /**
* 用不到了 * 用不到了这个类的作用是监听消息推送用的由admin推送一条config配置然后这里触发事件
* 现在改为直接由admin请求网关提供的接口进行配置修改
* 考虑
* *
* @author tanghc * @author tanghc
*/ */

@ -0,0 +1,68 @@
package com.gitee.sop.gatewaycommon.route;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author tanghc
*/
@Slf4j
public abstract class BaseRegistryListener implements RegistryListener {
private static final int FIVE_SECONDS = 1000 * 5;
private Map<String, Long> updateTimeMap = new ConcurrentHashMap<>(16);
public static List<String> EXCLUDE_SERVICE_ID_LIST = new ArrayList<>(8);
static {
EXCLUDE_SERVICE_ID_LIST.add("api-gateway");
EXCLUDE_SERVICE_ID_LIST.add("website-server");
}
@Autowired
private ServiceListener serviceListener;
/**
* 移除路由信息
*
* @param serviceId serviceId
*/
public void removeRoutes(String serviceId) {
serviceListener.onRemoveService(serviceId.toLowerCase());
}
/**
* 拉取路由信息
*
* @param instance 服务实例
*/
public void pullRoutes(InstanceDefinition instance) {
// serviceId统一小写
instance.setServiceId(instance.getServiceId().toLowerCase());
serviceListener.onAddInstance(instance);
}
protected boolean canOperator(String serviceId) {
for (String excludeServiceId : EXCLUDE_SERVICE_ID_LIST) {
if (excludeServiceId.equalsIgnoreCase(serviceId)) {
return false;
}
}
// nacos会不停的触发事件,这里做了一层拦截
// 同一个serviceId5秒内允许访问一次
Long lastUpdateTime = updateTimeMap.getOrDefault(serviceId, 0L);
long now = System.currentTimeMillis();
boolean can = now - lastUpdateTime > FIVE_SECONDS;
if (can) {
updateTimeMap.put(serviceId, now);
}
return can;
}
}

@ -1,154 +0,0 @@
package com.gitee.sop.gatewaycommon.route;
import com.alibaba.fastjson.JSON;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo;
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.DigestUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author tanghc
*/
@Slf4j
public abstract class BaseRoutesListener implements RegistryListener {
private static final String SOP_ROUTES_PATH = "/sop/routes";
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq";
private static final int FIVE_SECONDS = 1000 * 5;
private static final String METADATA_SERVER_CONTEXT_PATH = "server.servlet.context-path";
private static final String METADATA_SOP_ROUTES_PATH = "sop.routes.path";
private static RestTemplate restTemplate = new RestTemplate();
static {
// 解决statusCode不等于200,就抛异常问题
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected boolean hasError(HttpStatus statusCode) {
return statusCode == null;
}
});
}
private Map<String, Long> updateTimeMap = new ConcurrentHashMap<>(16);
@Autowired
private BaseRouteCache<?> baseRouteCache;
@Autowired
private RoutesProcessor routesProcessor;
/**
* 移除路由信息
*
* @param serviceId serviceId
*/
public void removeRoutes(String serviceId) {
doOperator(serviceId, () -> {
log.info("服务下线,删除路由配置,serviceId: {}", serviceId);
baseRouteCache.remove(serviceId);
routesProcessor.removeAllRoutes(serviceId);
});
}
/**
* 拉取路由信息
*
* @param instance 服务实例
*/
public void pullRoutes(InstanceDefinition instance) {
String serviceName = instance.getServiceId();
doOperator(serviceName, () -> {
String url = getRouteRequestUrl(instance);
log.info("拉取路由配置,serviceId: {}, url: {}", serviceName, url);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
String body = responseEntity.getBody();
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(body, ServiceRouteInfo.class);
baseRouteCache.load(serviceRouteInfo, callback -> routesProcessor.saveRoutes(serviceRouteInfo, instance));
} else {
log.error("拉取路由配置异常,url: {}, status: {}, body: {}", url, responseEntity.getStatusCodeValue(), responseEntity.getBody());
}
});
}
private void doOperator(String serviceId, Runnable runnable) {
if (canOperator(serviceId)) {
runnable.run();
}
}
private boolean canOperator(String serviceId) {
// nacos会不停的触发事件,这里做了一层拦截
// 同一个serviceId5秒内允许访问一次
Long lastUpdateTime = updateTimeMap.getOrDefault(serviceId, 0L);
long now = System.currentTimeMillis();
boolean can = now - lastUpdateTime > FIVE_SECONDS;
if (can) {
updateTimeMap.put(serviceId, now);
}
return can;
}
/**
* 拉取路由请求url
*
* @param instance 服务实例
* @return 返回最终url
*/
private static String getRouteRequestUrl(InstanceDefinition instance) {
Map<String, String> metadata = instance.getMetadata();
String customPath = metadata.get(METADATA_SOP_ROUTES_PATH);
String homeUrl;
String servletPath;
// 如果metadata中指定了获取路由的url
if (StringUtils.isNotBlank(customPath)) {
// 自定义完整的url
if (customPath.startsWith("http")) {
homeUrl = customPath;
servletPath = "";
} else {
homeUrl = getHomeUrl(instance);
servletPath = customPath;
}
} else {
// 默认处理
homeUrl = getHomeUrl(instance);
String contextPath = metadata.getOrDefault(METADATA_SERVER_CONTEXT_PATH, "");
servletPath = contextPath + SOP_ROUTES_PATH;
}
if (StringUtils.isNotBlank(servletPath) && !servletPath.startsWith("/")) {
servletPath = '/' + servletPath;
}
String query = buildQuery(SECRET);
return homeUrl + servletPath + query;
}
private static String getHomeUrl(InstanceDefinition instance) {
return "http://" + instance.getIp() + ":" + instance.getPort();
}
private static String buildQuery(String secret) {
String time = String.valueOf(System.currentTimeMillis());
String source = secret + time + secret;
String sign = DigestUtils.md5DigestAsHex(source.getBytes());
return "?time=" + time + "&sign=" + sign;
}
}

@ -0,0 +1,41 @@
package com.gitee.sop.gatewaycommon.route;
import org.springframework.http.HttpStatus;
import org.springframework.util.DigestUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
/**
* @author tanghc
*/
public abstract class BaseServiceListener implements ServiceListener {
private static final String SECRET = "a3d9sf!1@odl90zd>fkASwq";
private static RestTemplate restTemplate = new RestTemplate();
static {
// 解决statusCode不等于200,就抛异常问题
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected boolean hasError(HttpStatus statusCode) {
return statusCode == null;
}
});
}
protected static String buildQuery(String secret) {
String time = String.valueOf(System.currentTimeMillis());
String source = secret + time + secret;
String sign = DigestUtils.md5DigestAsHex(source.getBytes());
return "?time=" + time + "&sign=" + sign;
}
protected static String buildQuery() {
return buildQuery(SECRET);
}
public static RestTemplate getRestTemplate() {
return restTemplate;
}
}

@ -7,7 +7,6 @@ import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications; import com.netflix.discovery.shared.Applications;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient; import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
@ -22,7 +21,7 @@ import java.util.stream.Collectors;
* *
* @author tanghc * @author tanghc
*/ */
public class EurekaRoutesListener extends BaseRoutesListener { public class EurekaRegistryListener extends BaseRegistryListener {
static { static {
System.setProperty(SopPropertiesFactory.PROPERTIES_KEY, EurekaEnvironmentServerChooser.class.getName()); System.setProperty(SopPropertiesFactory.PROPERTIES_KEY, EurekaEnvironmentServerChooser.class.getName());
@ -30,9 +29,6 @@ public class EurekaRoutesListener extends BaseRoutesListener {
private Set<String> cacheServices = new HashSet<>(); private Set<String> cacheServices = new HashSet<>();
@Value("${spring.application.name:api-gateway}")
private String appName;
@Override @Override
public void onEvent(ApplicationEvent applicationEvent) { public void onEvent(ApplicationEvent applicationEvent) {
Object source = applicationEvent.getSource(); Object source = applicationEvent.getSource();
@ -44,15 +40,19 @@ public class EurekaRoutesListener extends BaseRoutesListener {
.map(Application::getName) .map(Application::getName)
.collect(Collectors.toList()); .collect(Collectors.toList());
Set<String> currentServices = new HashSet<>(serviceList); final Set<String> currentServices = new HashSet<>(serviceList);
currentServices.removeAll(cacheServices); currentServices.removeAll(cacheServices);
// 如果有新的服务注册进来 // 如果有新的服务注册进来
if (currentServices.size() > 0) { if (currentServices.size() > 0) {
this.doRegister(registeredApplications, currentServices); List<Application> newApplications = registeredApplications.stream()
.filter(application -> this.canOperator(application.getName())
&& currentServices.contains(application.getName()))
.collect(Collectors.toList());
this.doRegister(newApplications);
} }
currentServices = new HashSet<>(serviceList); cacheServices.removeAll(new HashSet<>(serviceList));
cacheServices.removeAll(currentServices);
// 如果有服务删除 // 如果有服务删除
if (cacheServices.size() > 0) { if (cacheServices.size() > 0) {
this.doRemove(cacheServices); this.doRemove(cacheServices);
@ -61,12 +61,8 @@ public class EurekaRoutesListener extends BaseRoutesListener {
cacheServices = new HashSet<>(serviceList); cacheServices = new HashSet<>(serviceList);
} }
private void doRegister(List<Application> registeredApplications, Set<String> newServices) { private void doRegister(List<Application> registeredApplications) {
registeredApplications registeredApplications.forEach(application -> {
.stream()
.filter(application -> !appName.equalsIgnoreCase(application.getName())
&& newServices.contains(application.getName()))
.forEach(application -> {
List<InstanceInfo> instances = application.getInstances(); List<InstanceInfo> instances = application.getInstances();
if (CollectionUtils.isNotEmpty(instances)) { if (CollectionUtils.isNotEmpty(instances)) {
instances.sort(Comparator.comparing(InstanceInfo::getLastUpdatedTimestamp).reversed()); instances.sort(Comparator.comparing(InstanceInfo::getLastUpdatedTimestamp).reversed());

@ -0,0 +1,71 @@
package com.gitee.sop.gatewaycommon.route;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
/**
* 加载服务路由nacos实现
*
* @author tanghc
*/
@Slf4j
public class NacosRegistryListener extends BaseRegistryListener {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@NacosInjected
private ConfigService configService;
@Override
public void onEvent(ApplicationEvent applicationEvent) {
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
List<ServiceInfo> subscribes = null;
try {
subscribes = namingService.getSubscribeServices();
} catch (NacosException e) {
log.error("namingService.getSubscribeServices()错误", e);
}
if (CollectionUtils.isEmpty(subscribes)) {
return;
}
subscribes.stream()
.filter(serviceInfo -> this.canOperator(serviceInfo.getName()))
.forEach(serviceInfo -> {
String serviceName = serviceInfo.getName();
try {
List<Instance> allInstances = namingService.getAllInstances(serviceName);
if (CollectionUtils.isEmpty(allInstances)) {
// 如果没有服务列表,则删除所有路由信息
removeRoutes(serviceName);
} else {
for (Instance instance : allInstances) {
InstanceDefinition instanceDefinition = new InstanceDefinition();
instanceDefinition.setInstanceId(instance.getInstanceId());
instanceDefinition.setServiceId(serviceName);
instanceDefinition.setIp(instance.getIp());
instanceDefinition.setPort(instance.getPort());
instanceDefinition.setMetadata(instance.getMetadata());
pullRoutes(instanceDefinition);
}
}
} catch (Exception e) {
log.error("选择服务实例失败,serviceName: {}", serviceName, e);
}
});
}
}

@ -1,75 +0,0 @@
package com.gitee.sop.gatewaycommon.route;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
/**
* 加载服务路由nacos实现
*
* @author tanghc
*/
@Slf4j
public class NacosRoutesListener extends BaseRoutesListener {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@NacosInjected
private ConfigService configService;
@Override
public void onEvent(ApplicationEvent applicationEvent) {
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
List<ServiceInfo> subscribes = null;
try {
subscribes = namingService.getSubscribeServices();
} catch (NacosException e) {
log.error("namingService.getSubscribeServices()错误", e);
}
if (CollectionUtils.isEmpty(subscribes)) {
return;
}
// subscribe
String thisServiceId = nacosDiscoveryProperties.getService();
for (ServiceInfo serviceInfo : subscribes) {
String serviceName = serviceInfo.getName();
// 如果是本机服务,跳过
if (Objects.equals(thisServiceId, serviceName)) {
continue;
}
try {
List<Instance> allInstances = namingService.getAllInstances(serviceName);
if (CollectionUtils.isEmpty(allInstances)) {
// 如果没有服务列表,则删除所有路由信息
removeRoutes(serviceName);
} else {
for (Instance instance : allInstances) {
InstanceDefinition instanceDefinition = new InstanceDefinition();
instanceDefinition.setInstanceId(instance.getInstanceId());
instanceDefinition.setServiceId(serviceName);
instanceDefinition.setIp(instance.getIp());
instanceDefinition.setPort(instance.getPort());
instanceDefinition.setMetadata(instance.getMetadata());
pullRoutes(instanceDefinition);
}
}
} catch (Exception e) {
log.error("选择服务实例失败,serviceName: {}", serviceName, e);
}
}
}
}

@ -0,0 +1,12 @@
package com.gitee.sop.gatewaycommon.route;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
/**
* @author tanghc
*/
public interface ServiceListener {
void onRemoveService(String serviceId);
void onAddInstance(InstanceDefinition instance);
}

@ -0,0 +1,92 @@
package com.gitee.sop.gatewaycommon.route;
import com.alibaba.fastjson.JSON;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo;
import com.gitee.sop.gatewaycommon.manager.BaseRouteCache;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.Map;
/**
* @author tanghc
*/
@Slf4j
public class ServiceRouteListener extends BaseServiceListener {
private static final String SOP_ROUTES_PATH = "/sop/routes";
private static final String METADATA_SERVER_CONTEXT_PATH = "server.servlet.context-path";
private static final String METADATA_SOP_ROUTES_PATH = "sop.routes.path";
@Autowired
private BaseRouteCache<?> baseRouteCache;
@Autowired
private RoutesProcessor routesProcessor;
@Override
public void onRemoveService(String serviceId) {
log.info("服务下线,删除路由配置,serviceId: {}", serviceId);
baseRouteCache.remove(serviceId);
routesProcessor.removeAllRoutes(serviceId);
}
@Override
public void onAddInstance(InstanceDefinition instance) {
String serviceName = instance.getServiceId();
String url = getRouteRequestUrl(instance);
log.info("拉取路由配置,serviceId: {}, url: {}", serviceName, url);
ResponseEntity<String> responseEntity = getRestTemplate().getForEntity(url, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
String body = responseEntity.getBody();
ServiceRouteInfo serviceRouteInfo = JSON.parseObject(body, ServiceRouteInfo.class);
baseRouteCache.load(serviceRouteInfo, callback -> routesProcessor.saveRoutes(serviceRouteInfo, instance));
} else {
log.error("拉取路由配置异常,url: {}, status: {}, body: {}", url, responseEntity.getStatusCodeValue(), responseEntity.getBody());
}
}
/**
* 拉取路由请求url
*
* @param instance 服务实例
* @return 返回最终url
*/
private static String getRouteRequestUrl(InstanceDefinition instance) {
Map<String, String> metadata = instance.getMetadata();
String customPath = metadata.get(METADATA_SOP_ROUTES_PATH);
String homeUrl;
String servletPath;
// 如果metadata中指定了获取路由的url
if (StringUtils.isNotBlank(customPath)) {
// 自定义完整的url
if (customPath.startsWith("http")) {
homeUrl = customPath;
servletPath = "";
} else {
homeUrl = getHomeUrl(instance);
servletPath = customPath;
}
} else {
// 默认处理
homeUrl = getHomeUrl(instance);
String contextPath = metadata.getOrDefault(METADATA_SERVER_CONTEXT_PATH, "");
servletPath = contextPath + SOP_ROUTES_PATH;
}
if (StringUtils.isNotBlank(servletPath) && !servletPath.startsWith("/")) {
servletPath = '/' + servletPath;
}
String query = buildQuery();
return homeUrl + servletPath + query;
}
private static String getHomeUrl(InstanceDefinition instance) {
return "http://" + instance.getIp() + ":" + instance.getPort();
}
}

@ -0,0 +1,38 @@
package com.gitee.sop.gatewaycommon.validate;
import com.gitee.sop.gatewaycommon.bean.SopConstants;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* @author tanghc
*/
public class SignConfig {
private static volatile Wrapper wrapper = new Wrapper() {};
public static void enableUrlencodeMode() {
wrapper = new Wrapper() {
@Override
public String wrapVal(Object val) {
String valStr = String.valueOf(val);
try {
return URLEncoder.encode(valStr, SopConstants.UTF8);
} catch (UnsupportedEncodingException e) {
return valStr;
}
}
};
}
public static String wrapVal(Object val) {
return wrapper.wrapVal(val);
}
interface Wrapper {
default String wrapVal(Object val) {
return String.valueOf(val);
}
}
}

@ -5,6 +5,7 @@
package com.gitee.sop.gatewaycommon.validate.alipay; package com.gitee.sop.gatewaycommon.validate.alipay;
import com.gitee.sop.gatewaycommon.message.ErrorEnum; import com.gitee.sop.gatewaycommon.message.ErrorEnum;
import com.gitee.sop.gatewaycommon.validate.SignConfig;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -188,13 +189,13 @@ public class AlipaySignature {
params.remove("sign"); params.remove("sign");
params.remove("sign_type"); params.remove("sign_type");
StringBuffer content = new StringBuffer(); StringBuilder content = new StringBuilder();
List<String> keys = new ArrayList<String>(params.keySet()); List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys); Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) { for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i); String key = keys.get(i);
String value = params.get(key); String value = SignConfig.wrapVal(params.get(key));
content.append((i == 0 ? "" : "&") + key + "=" + value); content.append((i == 0 ? "" : "&") + key + "=" + value);
} }
@ -208,13 +209,13 @@ public class AlipaySignature {
params.remove("sign"); params.remove("sign");
StringBuffer content = new StringBuffer(); StringBuilder content = new StringBuilder();
List<String> keys = new ArrayList<String>(params.keySet()); List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys); Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) { for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i); String key = keys.get(i);
String value = String.valueOf(params.get(key)); String value = SignConfig.wrapVal(params.get(key));
content.append((i == 0 ? "" : "&") + key + "=" + value); content.append((i == 0 ? "" : "&") + key + "=" + value);
} }

@ -4,6 +4,7 @@ package com.gitee.sop.gatewaycommon.validate.taobao;
import com.gitee.sop.gatewaycommon.message.ErrorEnum; import com.gitee.sop.gatewaycommon.message.ErrorEnum;
import com.gitee.sop.gatewaycommon.param.ApiParam; import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.validate.AbstractSigner; import com.gitee.sop.gatewaycommon.validate.AbstractSigner;
import com.gitee.sop.gatewaycommon.validate.SignConfig;
import com.gitee.sop.gatewaycommon.validate.SignEncipher; import com.gitee.sop.gatewaycommon.validate.SignEncipher;
import com.gitee.sop.gatewaycommon.validate.SignEncipherHMAC_MD5; import com.gitee.sop.gatewaycommon.validate.SignEncipherHMAC_MD5;
import com.gitee.sop.gatewaycommon.validate.SignEncipherMD5; import com.gitee.sop.gatewaycommon.validate.SignEncipherMD5;
@ -47,8 +48,8 @@ public class TaobaoSigner extends AbstractSigner {
// 第二步:把所有参数名和参数值串在一起 // 第二步:把所有参数名和参数值串在一起
StringBuilder paramNameValue = new StringBuilder(); StringBuilder paramNameValue = new StringBuilder();
for (String paramName : paramNames) { for (String paramName : paramNames) {
Object val = param.get(paramName); String val = SignConfig.wrapVal(param.get(paramName));
if (val != null && StringUtils.isNotBlank(String.valueOf(val))) { if (StringUtils.isNotBlank(val)) {
paramNameValue.append(paramName).append(val); paramNameValue.append(paramName).append(val);
} }
} }

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version> <version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>com.gitee.sop</groupId> <groupId>com.gitee.sop</groupId>
@ -16,13 +16,31 @@
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties> </properties>
<dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-dependencies</artifactId>
<version>0.9.0.RELEASE</version> <version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-gateway-common</artifactId>
<version>2.1.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency> </dependency>
<dependency> <dependency>

@ -3,13 +3,23 @@ package com.gitee.sop.websiteserver.config;
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.gitee.sop.gatewaycommon.route.EurekaRegistryListener;
import com.gitee.sop.gatewaycommon.route.NacosRegistryListener;
import com.gitee.sop.gatewaycommon.route.RegistryListener;
import com.gitee.sop.gatewaycommon.route.ServiceListener;
import com.gitee.sop.websiteserver.listener.ServiceDocListener;
import com.gitee.sop.websiteserver.manager.DocManager; import com.gitee.sop.websiteserver.manager.DocManager;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/** /**
* @author tanghc * @author tanghc
@ -20,6 +30,9 @@ public class WebsiteConfig implements ApplicationRunner {
@Autowired @Autowired
DocManager docManager; DocManager docManager;
@Autowired
private RegistryListener registryListener;
/** /**
* 使用fastjson代替jackson * 使用fastjson代替jackson
* @return * @return
@ -35,6 +48,37 @@ public class WebsiteConfig implements ApplicationRunner {
return new HttpMessageConverters(converter); return new HttpMessageConverters(converter);
} }
/**
* nacos事件监听
*
* @param heartbeatEvent
*/
@EventListener(classes = HeartbeatEvent.class)
public void listenNacosEvent(ApplicationEvent heartbeatEvent) {
registryListener.onEvent(heartbeatEvent);
}
/**
* 微服务路由加载
*/
@Bean
@ConditionalOnProperty("spring.cloud.nacos.discovery.server-addr")
RegistryListener registryListenerNacos() {
return new NacosRegistryListener();
}
@Bean
@ConditionalOnProperty("eureka.client.serviceUrl.defaultZone")
RegistryListener registryListenerEureka() {
return new EurekaRegistryListener();
}
@Bean
@ConditionalOnMissingBean
ServiceListener serviceListener() {
return new ServiceDocListener();
}
/** /**
* SpringBoot启动完毕执行 * SpringBoot启动完毕执行
*/ */

@ -0,0 +1,50 @@
package com.gitee.sop.websiteserver.listener;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.route.BaseServiceListener;
import com.gitee.sop.websiteserver.manager.DocManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @author tanghc
*/
@Slf4j
public class ServiceDocListener extends BaseServiceListener {
private static final String SECRET = "b749a2ec000f4f29";
@Autowired
private DocManager docManager;
@Override
public void onRemoveService(String serviceId) {
docManager.remove(serviceId);
}
@Override
public void onAddInstance(InstanceDefinition instance) {
String serviceId = instance.getServiceId();
String url = getRouteRequestUrl(instance);
ResponseEntity<String> responseEntity = getRestTemplate().getForEntity(url, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
String body = responseEntity.getBody();
docManager.addDocInfo(
serviceId
, body
, callback -> log.info("加载服务文档,serviceId={}, 机器={}"
, serviceId
, instance.getIp() + ":" + instance.getPort())
);
} else {
log.error("加载文档失败, status:{}, body:{}", responseEntity.getStatusCodeValue(), responseEntity.getBody());
}
}
private static String getRouteRequestUrl(InstanceDefinition instance) {
String query = buildQuery(SECRET);
return "http://" + instance.getIp() + ":" + instance.getPort() + "/v2/api-docs" + query;
}
}

@ -1,116 +0,0 @@
package com.gitee.sop.websiteserver.manager;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.api.naming.pojo.ServiceInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author tanghc
*/
@Slf4j
@Service
public class DocDiscovery {
private static final String SECRET = "b749a2ec000f4f29";
private static final int FIVE_SECONDS = 1000 * 5;
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
private RestTemplate restTemplate = new RestTemplate();
private Map<String, Long> updateTimeMap = new HashMap<>(16);
public DocDiscovery() {
// 解决statusCode不等于200,就抛异常问题
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected boolean hasError(HttpStatus statusCode) {
return statusCode == null;
}
});
}
public synchronized void refresh(DocManager docManager) {
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
List<ServiceInfo> subscribes = null;
try {
subscribes = namingService.getSubscribeServices();
} catch (NacosException e) {
log.error("namingService.getSubscribeServices()错误", e);
}
if (CollectionUtils.isEmpty(subscribes)) {
return;
}
// subscribe
String thisServiceId = nacosDiscoveryProperties.getService();
for (ServiceInfo serviceInfo : subscribes) {
String serviceId = serviceInfo.getName();
// 如果是本机服务,跳过
if (Objects.equals(thisServiceId, serviceId) || "api-gateway".equalsIgnoreCase(serviceId)) {
continue;
}
// nacos会不停的触发事件,这里做了一层拦截
// 同一个serviceId5秒内允许访问一次
Long lastUpdateTime = updateTimeMap.getOrDefault(serviceId, 0L);
long now = System.currentTimeMillis();
if (now - lastUpdateTime < FIVE_SECONDS) {
continue;
}
updateTimeMap.put(serviceId, now);
try {
List<Instance> allInstances = namingService.getAllInstances(serviceId);
if (CollectionUtils.isEmpty(allInstances)) {
// 如果没有服务列表,则删除所有路由信息
docManager.remove(serviceId);
} else {
for (Instance instance : allInstances) {
String url = getRouteRequestUrl(instance);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
String body = responseEntity.getBody();
docManager.addDocInfo(
serviceId
, body
, callback -> log.info("加载服务文档,serviceId={}, 机器={}"
, serviceId, instance.getIp() + ":" + instance.getPort())
);
}
}
}
} catch (NacosException e) {
log.error("选择服务实例失败,serviceId:{}", serviceId, e);
}
}
}
private static String getRouteRequestUrl(Instance instance) {
String query = buildQuery(SECRET);
return "http://" + instance.getIp() + ":" + instance.getPort() + "/v2/api-docs" + query;
}
private static String buildQuery(String secret) {
String time = String.valueOf(System.currentTimeMillis());
String source = secret + time + secret;
String sign = DigestUtils.md5DigestAsHex(source.getBytes());
return "?time=" + time + "&sign=" + sign;
}
}

@ -5,9 +5,6 @@ import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.Feature;
import com.gitee.sop.websiteserver.bean.DocInfo; import com.gitee.sop.websiteserver.bean.DocInfo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils; import org.springframework.util.DigestUtils;
@ -23,7 +20,7 @@ import java.util.function.Consumer;
*/ */
@Service @Service
@Slf4j @Slf4j
public class DocManagerImpl implements DocManager, ApplicationListener<HeartbeatEvent> { public class DocManagerImpl implements DocManager {
// key:title // key:title
private Map<String, DocInfo> docDefinitionMap = new HashMap<>(); private Map<String, DocInfo> docDefinitionMap = new HashMap<>();
@ -37,9 +34,6 @@ public class DocManagerImpl implements DocManager, ApplicationListener<Heartbeat
private DocParser easyopenDocParser = new EasyopenDocParser(); private DocParser easyopenDocParser = new EasyopenDocParser();
@Autowired
private DocDiscovery docDiscovery;
@Override @Override
public void addDocInfo(String serviceId, String docInfoJson, Consumer<DocInfo> callback) { public void addDocInfo(String serviceId, String docInfoJson, Consumer<DocInfo> callback) {
String newMd5 = DigestUtils.md5DigestAsHex(docInfoJson.getBytes(StandardCharsets.UTF_8)); String newMd5 = DigestUtils.md5DigestAsHex(docInfoJson.getBytes(StandardCharsets.UTF_8));
@ -80,10 +74,4 @@ public class DocManagerImpl implements DocManager, ApplicationListener<Heartbeat
serviceIdMd5Map.remove(serviceId); serviceIdMd5Map.remove(serviceId);
docDefinitionMap.entrySet().removeIf(entry -> serviceId.equalsIgnoreCase(entry.getValue().getServiceId())); docDefinitionMap.entrySet().removeIf(entry -> serviceId.equalsIgnoreCase(entry.getValue().getServiceId()));
} }
@Override
public void onApplicationEvent(HeartbeatEvent heartbeatEvent) {
docDiscovery.refresh(this);
}
} }

@ -2,12 +2,14 @@ server.port=8083
spring.application.name=website-server spring.application.name=website-server
# ------- 需要改的配置 ------- # ------- 需要改的配置 -------
# nacos地址 eureka.url=http://localhost:1111/eureka/
nacos.url=127.0.0.1:8848
# ------- 需要改的配置end ------- # ------- 需要改的配置end -------
# nacos cloud配置 # eureka注册中心
spring.cloud.nacos.discovery.server-addr=${nacos.url} eureka.client.serviceUrl.defaultZone=${eureka.url}
## nacos cloud配置
#spring.cloud.nacos.discovery.server-addr=${nacos.url}
# 测试环境 # 测试环境
api.url-test=http://open-test.yourdomain.com api.url-test=http://open-test.yourdomain.com

Loading…
Cancel
Save