diff --git a/changelog.md b/changelog.md index d7bfeb84..e6f0e9da 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## 2.2.0 - 支持eureka注册中心,需要执行`sop-2.2.0.sql`升级文件 +- 签名内容支持urlencode(设置`sign.urlencode=true`) ## 2.1.3 diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java index af52ea31..8e91db5f 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java @@ -8,15 +8,17 @@ import com.gitee.sop.gatewaycommon.limit.LimitManager; import com.gitee.sop.gatewaycommon.loadbalancer.SopPropertiesFactory; import com.gitee.sop.gatewaycommon.message.ErrorFactory; import com.gitee.sop.gatewaycommon.param.ParameterFormatter; -import com.gitee.sop.gatewaycommon.route.EurekaRoutesListener; -import com.gitee.sop.gatewaycommon.route.NacosRoutesListener; +import com.gitee.sop.gatewaycommon.route.ServiceRouteListener; +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.gatewaycommon.secret.IsvManager; import com.gitee.sop.gatewaycommon.session.SessionManager; +import com.gitee.sop.gatewaycommon.validate.SignConfig; import com.gitee.sop.gatewaycommon.validate.Validator; import org.springframework.beans.BeansException; 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.ConditionalOnProperty; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; @@ -72,13 +74,19 @@ public class AbstractConfiguration implements ApplicationContextAware { @Bean @ConditionalOnProperty("spring.cloud.nacos.discovery.server-addr") RegistryListener registryListenerNacos() { - return new NacosRoutesListener(); + return new NacosRegistryListener(); } @Bean @ConditionalOnProperty("eureka.client.serviceUrl.defaultZone") RegistryListener registryListenerEureka() { - return new EurekaRoutesListener(); + return new EurekaRegistryListener(); + } + + @Bean + @ConditionalOnMissingBean + ServiceListener serviceListener() { + return new ServiceRouteListener(); } @Bean @@ -174,6 +182,14 @@ public class AbstractConfiguration implements ApplicationContextAware { if (RouteRepositoryContext.getRouteRepository() == null) { 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); initMessage(); diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/NacosEventProcessor.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/NacosEventProcessor.java index 949b3250..ff8ab7a6 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/NacosEventProcessor.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/NacosEventProcessor.java @@ -14,7 +14,9 @@ import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; /** - * 用不到了 + * 用不到了,这个类的作用是监听消息推送用的。由admin推送一条config配置,然后这里触发事件。 + * 现在改为直接由admin请求网关提供的接口进行配置修改。 + * 考虑 * * @author tanghc */ diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRegistryListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRegistryListener.java new file mode 100644 index 00000000..54fa268a --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRegistryListener.java @@ -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 updateTimeMap = new ConcurrentHashMap<>(16); + + public static List 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; + } +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRoutesListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRoutesListener.java deleted file mode 100644 index f73e8039..00000000 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseRoutesListener.java +++ /dev/null @@ -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 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 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 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; - } -} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseServiceListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseServiceListener.java new file mode 100644 index 00000000..cb1f5339 --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/BaseServiceListener.java @@ -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; + } +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRoutesListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java similarity index 55% rename from sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRoutesListener.java rename to sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java index 88809254..954cf1cf 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRoutesListener.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java @@ -7,7 +7,6 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Applications; import org.apache.commons.collections.CollectionUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.netflix.eureka.CloudEurekaClient; import org.springframework.context.ApplicationEvent; @@ -22,7 +21,7 @@ import java.util.stream.Collectors; * * @author tanghc */ -public class EurekaRoutesListener extends BaseRoutesListener { +public class EurekaRegistryListener extends BaseRegistryListener { static { System.setProperty(SopPropertiesFactory.PROPERTIES_KEY, EurekaEnvironmentServerChooser.class.getName()); @@ -30,9 +29,6 @@ public class EurekaRoutesListener extends BaseRoutesListener { private Set cacheServices = new HashSet<>(); - @Value("${spring.application.name:api-gateway}") - private String appName; - @Override public void onEvent(ApplicationEvent applicationEvent) { Object source = applicationEvent.getSource(); @@ -44,15 +40,19 @@ public class EurekaRoutesListener extends BaseRoutesListener { .map(Application::getName) .collect(Collectors.toList()); - Set currentServices = new HashSet<>(serviceList); + final Set currentServices = new HashSet<>(serviceList); currentServices.removeAll(cacheServices); // 如果有新的服务注册进来 if (currentServices.size() > 0) { - this.doRegister(registeredApplications, currentServices); + List 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(currentServices); + cacheServices.removeAll(new HashSet<>(serviceList)); // 如果有服务删除 if (cacheServices.size() > 0) { this.doRemove(cacheServices); @@ -61,25 +61,21 @@ public class EurekaRoutesListener extends BaseRoutesListener { cacheServices = new HashSet<>(serviceList); } - private void doRegister(List registeredApplications, Set newServices) { - registeredApplications - .stream() - .filter(application -> !appName.equalsIgnoreCase(application.getName()) - && newServices.contains(application.getName())) - .forEach(application -> { - List instances = application.getInstances(); - if (CollectionUtils.isNotEmpty(instances)) { - instances.sort(Comparator.comparing(InstanceInfo::getLastUpdatedTimestamp).reversed()); - InstanceInfo instanceInfo = instances.get(0); - InstanceDefinition instanceDefinition = new InstanceDefinition(); - instanceDefinition.setInstanceId(instanceInfo.getInstanceId()); - instanceDefinition.setServiceId(instanceInfo.getAppName()); - instanceDefinition.setIp(instanceInfo.getIPAddr()); - instanceDefinition.setPort(instanceInfo.getPort()); - instanceDefinition.setMetadata(instanceInfo.getMetadata()); - pullRoutes(instanceDefinition); - } - }); + private void doRegister(List registeredApplications) { + registeredApplications.forEach(application -> { + List instances = application.getInstances(); + if (CollectionUtils.isNotEmpty(instances)) { + instances.sort(Comparator.comparing(InstanceInfo::getLastUpdatedTimestamp).reversed()); + InstanceInfo instanceInfo = instances.get(0); + InstanceDefinition instanceDefinition = new InstanceDefinition(); + instanceDefinition.setInstanceId(instanceInfo.getInstanceId()); + instanceDefinition.setServiceId(instanceInfo.getAppName()); + instanceDefinition.setIp(instanceInfo.getIPAddr()); + instanceDefinition.setPort(instanceInfo.getPort()); + instanceDefinition.setMetadata(instanceInfo.getMetadata()); + pullRoutes(instanceDefinition); + } + }); } private void doRemove(Set deletedServices) { diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRegistryListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRegistryListener.java new file mode 100644 index 00000000..73c2494a --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRegistryListener.java @@ -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 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 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); + } + }); + } + +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRoutesListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRoutesListener.java deleted file mode 100644 index e7abd8e0..00000000 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosRoutesListener.java +++ /dev/null @@ -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 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 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); - } - } - } - -} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceListener.java new file mode 100644 index 00000000..d0f1644d --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceListener.java @@ -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); +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceRouteListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceRouteListener.java new file mode 100644 index 00000000..0d5f050d --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceRouteListener.java @@ -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 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 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(); + } +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignConfig.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignConfig.java new file mode 100644 index 00000000..928b57a4 --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/SignConfig.java @@ -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); + } + } + +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java index 2ad8bb75..2c769afc 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/alipay/AlipaySignature.java @@ -5,6 +5,7 @@ package com.gitee.sop.gatewaycommon.validate.alipay; import com.gitee.sop.gatewaycommon.message.ErrorEnum; +import com.gitee.sop.gatewaycommon.validate.SignConfig; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; @@ -188,13 +189,13 @@ public class AlipaySignature { params.remove("sign"); params.remove("sign_type"); - StringBuffer content = new StringBuffer(); + StringBuilder content = new StringBuilder(); List keys = new ArrayList(params.keySet()); Collections.sort(keys); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); - String value = params.get(key); + String value = SignConfig.wrapVal(params.get(key)); content.append((i == 0 ? "" : "&") + key + "=" + value); } @@ -208,13 +209,13 @@ public class AlipaySignature { params.remove("sign"); - StringBuffer content = new StringBuffer(); + StringBuilder content = new StringBuilder(); List keys = new ArrayList(params.keySet()); Collections.sort(keys); for (int i = 0; i < keys.size(); 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); } diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java index 00b33884..57876fcd 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/taobao/TaobaoSigner.java @@ -4,6 +4,7 @@ package com.gitee.sop.gatewaycommon.validate.taobao; import com.gitee.sop.gatewaycommon.message.ErrorEnum; import com.gitee.sop.gatewaycommon.param.ApiParam; 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.SignEncipherHMAC_MD5; import com.gitee.sop.gatewaycommon.validate.SignEncipherMD5; @@ -47,8 +48,8 @@ public class TaobaoSigner extends AbstractSigner { // 第二步:把所有参数名和参数值串在一起 StringBuilder paramNameValue = new StringBuilder(); for (String paramName : paramNames) { - Object val = param.get(paramName); - if (val != null && StringUtils.isNotBlank(String.valueOf(val))) { + String val = SignConfig.wrapVal(param.get(paramName)); + if (StringUtils.isNotBlank(val)) { paramNameValue.append(paramName).append(val); } } diff --git a/sop-website/pom.xml b/sop-website/pom.xml index 56cf18b7..7cf06123 100644 --- a/sop-website/pom.xml +++ b/sop-website/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 2.1.2.RELEASE com.gitee.sop @@ -16,13 +16,31 @@ 1.8 + Greenwich.RELEASE + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + com.gitee.sop + sop-gateway-common + 2.1.3-SNAPSHOT + + org.springframework.cloud - spring-cloud-alibaba-nacos-discovery - 0.9.0.RELEASE + spring-cloud-starter-netflix-eureka-client diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java index cdf8d8ae..335e0efb 100644 --- a/sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java +++ b/sop-website/src/main/java/com/gitee/sop/websiteserver/config/WebsiteConfig.java @@ -3,13 +3,23 @@ package com.gitee.sop.websiteserver.config; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; 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.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; /** * @author tanghc @@ -20,6 +30,9 @@ public class WebsiteConfig implements ApplicationRunner { @Autowired DocManager docManager; + @Autowired + private RegistryListener registryListener; + /** * 使用fastjson代替jackson * @return @@ -35,6 +48,37 @@ public class WebsiteConfig implements ApplicationRunner { 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启动完毕执行 */ diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/listener/ServiceDocListener.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/listener/ServiceDocListener.java new file mode 100644 index 00000000..dd3eed10 --- /dev/null +++ b/sop-website/src/main/java/com/gitee/sop/websiteserver/listener/ServiceDocListener.java @@ -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 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; + } +} diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocDiscovery.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocDiscovery.java deleted file mode 100644 index 55d1a38b..00000000 --- a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocDiscovery.java +++ /dev/null @@ -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 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 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 allInstances = namingService.getAllInstances(serviceId); - if (CollectionUtils.isEmpty(allInstances)) { - // 如果没有服务列表,则删除所有路由信息 - docManager.remove(serviceId); - } else { - for (Instance instance : allInstances) { - String url = getRouteRequestUrl(instance); - ResponseEntity 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; - } - -} diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManagerImpl.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManagerImpl.java index 9d7e3114..7667f5a6 100644 --- a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManagerImpl.java +++ b/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/DocManagerImpl.java @@ -5,9 +5,6 @@ import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.Feature; import com.gitee.sop.websiteserver.bean.DocInfo; 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.util.DigestUtils; @@ -23,7 +20,7 @@ import java.util.function.Consumer; */ @Service @Slf4j -public class DocManagerImpl implements DocManager, ApplicationListener { +public class DocManagerImpl implements DocManager { // key:title private Map docDefinitionMap = new HashMap<>(); @@ -37,9 +34,6 @@ public class DocManagerImpl implements DocManager, ApplicationListener callback) { String newMd5 = DigestUtils.md5DigestAsHex(docInfoJson.getBytes(StandardCharsets.UTF_8)); @@ -80,10 +74,4 @@ public class DocManagerImpl implements DocManager, ApplicationListener serviceId.equalsIgnoreCase(entry.getValue().getServiceId())); } - - @Override - public void onApplicationEvent(HeartbeatEvent heartbeatEvent) { - docDiscovery.refresh(this); - } - } diff --git a/sop-website/src/main/resources/application-dev.properties b/sop-website/src/main/resources/application-dev.properties index 32eaeb91..3c02a777 100644 --- a/sop-website/src/main/resources/application-dev.properties +++ b/sop-website/src/main/resources/application-dev.properties @@ -2,12 +2,14 @@ server.port=8083 spring.application.name=website-server # ------- 需要改的配置 ------- -# nacos地址 -nacos.url=127.0.0.1:8848 +eureka.url=http://localhost:1111/eureka/ # ------- 需要改的配置end ------- -# nacos cloud配置 -spring.cloud.nacos.discovery.server-addr=${nacos.url} +# eureka注册中心 +eureka.client.serviceUrl.defaultZone=${eureka.url} + +## nacos cloud配置 +#spring.cloud.nacos.discovery.server-addr=${nacos.url} # 测试环境 api.url-test=http://open-test.yourdomain.com