diff --git a/README.md b/README.md index 2b2f2e2c..3acda6c4 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ SOP封装了开放平台大部分功能包括:签名验证、统一异常处 - 接入方管理+秘钥管理 - 接口权限分配 - 文件上传/下载 -- SDK +- 提供基础SDK(含:Java,C#,Python,Go) - 接口限流 - 文档整合 - 应用授权 diff --git a/changelog.md b/changelog.md index c6822c03..03f668dd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,15 @@ # changelog +## 3.2.1 + +- 强化RouteInterceptorContext,可获取微服务信息 + ## 3.2.0 - 使用alibaba cloud +- 新增Python,Go版本SDK +- 返回结果新增全局request_id +- 沙箱环境可填写token Hoxton.SR3(Spring Cloud Version), 2.2.1.RELEASE(Spring Cloud Alibaba Version), 2.2.5.RELEASE(Spring Boot Version) diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/DefaultRouteInterceptorContext.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/DefaultRouteInterceptorContext.java index 7fc5497b..abc7aea4 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/DefaultRouteInterceptorContext.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/DefaultRouteInterceptorContext.java @@ -2,6 +2,7 @@ package com.gitee.sop.gatewaycommon.bean; import com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext; import com.gitee.sop.gatewaycommon.param.ApiParam; +import org.springframework.cloud.client.ServiceInstance; /** * @author tanghc @@ -26,6 +27,8 @@ public class DefaultRouteInterceptorContext implements RouteInterceptorContext { private long requestDataSize; /** 返回内容大小 */ private long responseDataSize; + /** 负载均衡选中的微服务 */ + private ServiceInstance serviceInstance; @Override public ApiParam getApiParam() { @@ -111,4 +114,13 @@ public class DefaultRouteInterceptorContext implements RouteInterceptorContext { public void setResponseDataSize(long responseDataSize) { this.responseDataSize = responseDataSize; } + + @Override + public ServiceInstance getServiceInstance() { + return serviceInstance; + } + + public void setServiceInstance(ServiceInstance serviceInstance) { + this.serviceInstance = serviceInstance; + } } diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java index 1e176a42..beb5e859 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java @@ -51,5 +51,6 @@ public class SopConstants { public static final String METADATA_ENV_GRAY_VALUE = "gray"; public static final String CACHE_ROUTE_INTERCEPTOR_CONTEXT = "cacheRouteInterceptorContext"; + public static final String TARGET_SERVICE = "sop-target-service"; } diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/ServerWebExchangeUtil.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/ServerWebExchangeUtil.java index 88634b27..502178ed 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/ServerWebExchangeUtil.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/ServerWebExchangeUtil.java @@ -30,13 +30,10 @@ import reactor.core.publisher.Mono; import javax.servlet.http.HttpServletRequest; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Consumer; /** @@ -49,11 +46,7 @@ public class ServerWebExchangeUtil { private static final String UNKNOWN_PATH = "/sop/unknown"; private static final String REST_PATH = "/rest"; - private static final String IP_UNKNOWN = "unknown"; - private static final String IP_LOCAL = "127.0.0.1"; - private static final int IP_LEN = 15; - - private static FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter(); + private static final FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter(); private static final List> messageReaders; @@ -119,12 +112,10 @@ public class ServerWebExchangeUtil { public static ApiParam getApiParamByQuery(ServerWebExchange exchange, String query) { ApiParam apiParam = new ApiParam(); - String ip = getIP(exchange.getRequest()); + String ip = RequestUtil.getIP(exchange.getRequest()); apiParam.setIp(ip); Map params = RequestUtil.parseQueryToMap(query); - if (params != null) { - apiParam.putAll(params); - } + apiParam.putAll(params); setApiParam(exchange, apiParam); return apiParam; } @@ -135,7 +126,7 @@ public class ServerWebExchangeUtil { contentType = MediaType.APPLICATION_FORM_URLENCODED; } ApiParam apiParam = new ApiParam(); - String ip = getIP(exchange.getRequest()); + String ip = RequestUtil.getIP(exchange.getRequest()); apiParam.setIp(ip); Map params = null; String contentTypeStr = contentType.toString().toLowerCase(); @@ -161,44 +152,6 @@ public class ServerWebExchangeUtil { return apiParam; } - /** - * 获取客户端真实ip - * @param request request - * @return 返回ip - */ - public static String getIP(ServerHttpRequest request) { - HttpHeaders headers = request.getHeaders(); - String ipAddress = headers.getFirst("x-forwarded-for"); - if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { - ipAddress = headers.getFirst("Proxy-Client-IP"); - } - if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { - ipAddress = headers.getFirst("WL-Proxy-Client-IP"); - } - if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { - ipAddress = Optional.ofNullable(request.getRemoteAddress()) - .map(address -> address.getAddress().getHostAddress()) - .orElse(""); - if (IP_LOCAL.equals(ipAddress)) { - // 根据网卡取本机配置的IP - try { - InetAddress inet = InetAddress.getLocalHost(); - ipAddress = inet.getHostAddress(); - } catch (UnknownHostException e) { - // ignore - } - } - } - - // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 - if (ipAddress != null && ipAddress.length() > IP_LEN) { - int index = ipAddress.indexOf(","); - if (index > 0) { - ipAddress = ipAddress.substring(0, index); - } - } - return ipAddress; - } public static ApiParam getApiParam(ServerWebExchange exchange, Map params) { ApiParam apiParam = new ApiParam(); diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/IndexFilter.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/IndexFilter.java index df32dc52..0d9801b9 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/IndexFilter.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/IndexFilter.java @@ -32,6 +32,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; +import java.util.Arrays; +import java.util.List; import java.util.Objects; /** @@ -44,7 +46,11 @@ import java.util.Objects; public class IndexFilter implements WebFilter { private static final String REST_PATH_PREFIX = "/rest"; - private static final String SOP_PATH_PREFIX = "/sop"; + + /** 路径白名单 */ + private static List PATH_WHITE_LIST = Arrays.asList( + "/sop", "/actuator" + ); @Value("${sop.gateway-index-path:/}") private String indexPath; @@ -59,8 +65,8 @@ public class IndexFilter implements WebFilter { public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); - // SOP路径,直接放行 - if (path.startsWith(SOP_PATH_PREFIX)) { + // 路径是否在白名单中,直接放行 + if (this.isPathInWhiteList(path)) { return chain.filter(exchange); } // 如果是restful请求,直接转发 @@ -125,6 +131,15 @@ public class IndexFilter implements WebFilter { } } + private boolean isPathInWhiteList(String path) { + for (String whitePath : PATH_WHITE_LIST) { + if (path.startsWith(whitePath)) { + return true; + } + } + return false; + } + private void doValidate(ServerWebExchange exchange, ApiParam apiParam) { try { validator.validate(apiParam); diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/SopLoadBalancerClientFilter.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/SopLoadBalancerClientFilter.java index b8e74c55..b048c8af 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/SopLoadBalancerClientFilter.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/SopLoadBalancerClientFilter.java @@ -1,5 +1,6 @@ package com.gitee.sop.gatewaycommon.gateway.filter; +import com.gitee.sop.gatewaycommon.bean.SopConstants; import com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; @@ -22,11 +23,14 @@ public class SopLoadBalancerClientFilter extends LoadBalancerClientFilter { @Override protected ServiceInstance choose(ServerWebExchange exchange) { + ServiceInstance serviceInstance; if (loadBalancer instanceof SopLoadBalancerClient) { SopLoadBalancerClient sopLoadBalancerClient = (SopLoadBalancerClient)loadBalancer; - return sopLoadBalancerClient.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(), exchange); + serviceInstance = sopLoadBalancerClient.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(), exchange); } else { - return super.choose(exchange); + serviceInstance = super.choose(exchange); } + exchange.getAttributes().put(SopConstants.TARGET_SERVICE, serviceInstance); + return serviceInstance; } } diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/result/GatewayResultExecutor.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/result/GatewayResultExecutor.java index 87df32b9..03eab2c4 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/result/GatewayResultExecutor.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/result/GatewayResultExecutor.java @@ -2,6 +2,7 @@ package com.gitee.sop.gatewaycommon.gateway.result; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import com.gitee.sop.gatewaycommon.bean.DefaultRouteInterceptorContext; import com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext; import com.gitee.sop.gatewaycommon.bean.SopConstants; import com.gitee.sop.gatewaycommon.exception.ApiException; @@ -12,6 +13,7 @@ import com.gitee.sop.gatewaycommon.param.ApiParam; import com.gitee.sop.gatewaycommon.result.BaseExecutorAdapter; import com.gitee.sop.gatewaycommon.result.ResultExecutorForGateway; import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -71,6 +73,13 @@ public class GatewayResultExecutor extends BaseExecutorAdapter implements ResultExecutor { - private static Map HTTP_STATUS_ERROR_ENUM_MAP = new HashMap<>(8); + private static final Map HTTP_STATUS_ERROR_ENUM_MAP = new HashMap<>(8); private static final String GATEWAY_CODE_NAME = "code"; private static final String GATEWAY_MSG_NAME = "msg"; @@ -107,7 +107,7 @@ public abstract class BaseExecutorAdapter implements ResultExecutor * * @param serviceResult 微服务结果 * @param responseStatus 微服务状态码 - * @param requestContext 微服务状态码 + * @param requestContext 请求上下文 */ private void doAfterRoute(String serviceResult, int responseStatus, T requestContext) { RouteInterceptorContext routeInterceptorContext = getRouteInterceptorContext(requestContext); @@ -125,9 +125,14 @@ public abstract class BaseExecutorAdapter implements ResultExecutor defaultRouteInterceptorContext.setServiceErrorMsg(responseErrorMessage); } } + this.bindRouteInterceptorContextProperties(routeInterceptorContext, requestContext); RouteInterceptorUtil.runAfterRoute(routeInterceptorContext); } + protected void bindRouteInterceptorContextProperties(RouteInterceptorContext routeInterceptorContext, T requestContext) { + + } + /** * 将微服务的返回结果解析成JSONObject * @@ -145,6 +150,11 @@ public abstract class BaseExecutorAdapter implements ResultExecutor } ErrorMeta errorMeta = errorEnum.getErrorMeta(); JSONObject responseData = JSON.parseObject(serviceResult); + ApiParam apiParam = this.getApiParam(request); + if (apiParam != null) { + // 全局请求id,方便追踪定位 + responseData.put("request_id", apiParam.fetchRequestId()); + } responseData.put(GATEWAY_CODE_NAME, errorMeta.getCode()); responseData.put(GATEWAY_MSG_NAME, errorMeta.getError(getLocale(request)).getMsg()); return responseData; diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java index 0e481a40..61cc436e 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/EurekaRegistryListener.java @@ -5,6 +5,7 @@ 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.Autowired; import org.springframework.cloud.netflix.eureka.CloudEurekaClient; import org.springframework.context.ApplicationEvent; @@ -23,6 +24,9 @@ public class EurekaRegistryListener extends BaseRegistryListener { private Set cacheServices = new HashSet<>(); + @Autowired(required = false) + private List registryEventList; + @Override public void onEvent(ApplicationEvent applicationEvent) { Object source = applicationEvent.getSource(); @@ -57,10 +61,8 @@ public class EurekaRegistryListener extends BaseRegistryListener { } Set removedServiceIdList = getRemovedServiceId(serviceList); - // 如果有服务删除 - if (removedServiceIdList.size() > 0) { - this.doRemove(removedServiceIdList); - } + // 如果有服务下线 + this.doRemove(removedServiceIdList); cacheServices = new HashSet<>(serviceList); } @@ -106,12 +108,23 @@ public class EurekaRegistryListener extends BaseRegistryListener { instanceDefinition.setPort(instanceInfo.getPort()); instanceDefinition.setMetadata(instanceInfo.getMetadata()); pullRoutes(instanceDefinition); + if (registryEventList != null) { + registryEventList.forEach(registryEvent -> registryEvent.onRegistry(instanceDefinition)); + } } }); } private void doRemove(Set deletedServices) { - deletedServices.forEach(this::removeRoutes); + if (deletedServices == null) { + return; + } + deletedServices.forEach(serviceId -> { + this.removeRoutes(serviceId); + if (registryEventList != null) { + registryEventList.forEach(registryEvent -> registryEvent.onRemove(serviceId)); + } + }); } } 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 index a514f1b1..75519137 100644 --- 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 @@ -6,16 +6,15 @@ import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.api.naming.pojo.ListView; import com.gitee.sop.gatewaycommon.bean.InstanceDefinition; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEvent; import org.springframework.util.CollectionUtils; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -28,7 +27,9 @@ import java.util.stream.Collectors; @Slf4j public class NacosRegistryListener extends BaseRegistryListener { - private volatile Set cacheServices = new HashSet<>(); + private static final String METADATA_KEY_TIME_STARTUP = "time.startup"; + + private volatile Set cacheServices = new HashSet<>(); @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @@ -38,6 +39,41 @@ public class NacosRegistryListener extends BaseRegistryListener { @Override public synchronized void onEvent(ApplicationEvent applicationEvent) { + List serviceList = this.getServiceList(); + final Set currentServices = new HashSet<>(serviceList); + + // 删除现有的,剩下的就是新服务 + currentServices.removeAll(cacheServices); + // 如果有新的服务注册进来 + if (currentServices.size() > 0) { + currentServices.forEach(nacosServiceHolder -> { + Instance instance = nacosServiceHolder.getInstance(); + InstanceDefinition instanceDefinition = new InstanceDefinition(); + instanceDefinition.setInstanceId(instance.getInstanceId()); + instanceDefinition.setServiceId(nacosServiceHolder.getServiceId()); + instanceDefinition.setIp(instance.getIp()); + instanceDefinition.setPort(instance.getPort()); + instanceDefinition.setMetadata(instance.getMetadata()); + pullRoutes(instanceDefinition); + if (registryEventList != null) { + registryEventList.forEach(registryEvent -> registryEvent.onRegistry(instanceDefinition)); + } + }); + } + + // 如果有服务下线 + Set removedServiceIdList = getRemovedServiceId(serviceList); + // 移除 + this.doRemove(removedServiceIdList); + + cacheServices = new HashSet<>(serviceList); + } + + /** + * 获取建康的服务实例 + * @return 没有返回空的list + */ + private List getServiceList() { NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); ListView servicesOfServer = null; try { @@ -46,67 +82,42 @@ public class NacosRegistryListener extends BaseRegistryListener { log.error("namingService.getServicesOfServer()错误", e); } if (servicesOfServer == null || CollectionUtils.isEmpty(servicesOfServer.getData())) { - return; + return Collections.emptyList(); } - - Map instanceMap = servicesOfServer + return servicesOfServer .getData() .stream() .filter(this::canOperator) .map(serviceName -> { try { + // 获取服务实例 List allInstances = namingService.getAllInstances(serviceName); if (CollectionUtils.isEmpty(allInstances)) { return null; } Instance instance = allInstances.stream() + // 只获取建康实例 .filter(Instance::isHealthy) - .findFirst() + // 根据启动时间倒叙,找到最新的服务器 + .max(Comparator.comparing(ins -> { + String startupTime = ins.getMetadata().getOrDefault(METADATA_KEY_TIME_STARTUP, "0"); + return Long.valueOf(startupTime); + })) .orElse(null); - return instance == null ? null : new InstanceInfo(serviceName, instance); + if (instance == null) { + return null; + } else { + String startupTime = instance.getMetadata().getOrDefault("time.startup", "0"); + long time = Long.parseLong(startupTime); + return new NacosServiceHolder(serviceName, time, instance); + } } catch (NacosException e) { log.error("namingService.getAllInstances(serviceName)错误,serviceName:{}", serviceName, e); return null; } }) .filter(Objects::nonNull) - .collect(Collectors.toMap(InstanceInfo::getServiceName, InstanceInfo::getInstance)); - - - Set serviceNames = instanceMap.keySet(); - - final Set currentServices = new HashSet<>(serviceNames); - // 删除现有的,剩下的就是新服务 - currentServices.removeAll(cacheServices); - // 如果有新的服务注册进来 - if (currentServices.size() > 0) { - currentServices.forEach(serviceName -> { - Instance instance = instanceMap.get(serviceName); - 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); - if (registryEventList != null) { - registryEventList.forEach(registryEvent -> registryEvent.onRegistry(instanceDefinition)); - } - }); - } - - // 如果有服务删除 - Set removedServiceIdList = getRemovedServiceId(serviceNames); - if (removedServiceIdList.size() > 0) { - removedServiceIdList.forEach(serviceId->{ - this.removeRoutes(serviceId); - if (registryEventList != null) { - registryEventList.forEach(registryEvent -> registryEvent.onRemove(serviceId)); - } - }); - } - - cacheServices = new HashSet<>(serviceNames); + .collect(Collectors.toList()); } /** @@ -115,18 +126,29 @@ public class NacosRegistryListener extends BaseRegistryListener { * @param serviceList 最新的serviceId集合 * @return 返回已下线的serviceId */ - private Set getRemovedServiceId(Set serviceList) { - Set cache = cacheServices; - // 删除最新的,剩下就是已经删除的 - cache.removeAll(serviceList); + private Set getRemovedServiceId(List serviceList) { + Set cache = cacheServices.stream() + .map(NacosServiceHolder::getServiceId) + .collect(Collectors.toSet()); + + Set newList = serviceList.stream() + .map(NacosServiceHolder::getServiceId) + .collect(Collectors.toSet()); + + cache.removeAll(newList); return cache; } - @Data - @AllArgsConstructor - private static class InstanceInfo { - private String serviceName; - private Instance instance; + private void doRemove(Set deletedServices) { + if (deletedServices == null) { + return; + } + deletedServices.forEach(serviceId -> { + this.removeRoutes(serviceId); + if (registryEventList != null) { + registryEventList.forEach(registryEvent -> registryEvent.onRemove(serviceId)); + } + }); } } diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosServiceHolder.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosServiceHolder.java new file mode 100644 index 00000000..0b022f62 --- /dev/null +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/NacosServiceHolder.java @@ -0,0 +1,20 @@ +package com.gitee.sop.gatewaycommon.route; + +import com.alibaba.nacos.api.naming.pojo.Instance; + +/** + * @author tanghc + */ +public class NacosServiceHolder extends ServiceHolder { + + private Instance instance; + + public NacosServiceHolder(String serviceId, long lastUpdatedTimestamp, Instance instance) { + super(serviceId, lastUpdatedTimestamp); + this.instance = instance; + } + + public Instance getInstance() { + return instance; + } +} diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceHolder.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceHolder.java index 1febf746..a5641f08 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceHolder.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/route/ServiceHolder.java @@ -2,10 +2,29 @@ package com.gitee.sop.gatewaycommon.route; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; +import lombok.Setter; -@Data +import java.util.Objects; + +@Getter +@Setter @AllArgsConstructor public class ServiceHolder { private String serviceId; private long lastUpdatedTimestamp; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceHolder that = (ServiceHolder) o; + return lastUpdatedTimestamp == that.lastUpdatedTimestamp && + serviceId.equals(that.serviceId); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId, lastUpdatedTimestamp); + } } \ No newline at end of file diff --git a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java index 8c802640..a71d03bb 100644 --- a/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java +++ b/sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java @@ -15,8 +15,10 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.commons.CommonsMultipartFile; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; @@ -36,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -46,7 +49,7 @@ import java.util.stream.Collectors; */ public class RequestUtil { - private static Logger log = LoggerFactory.getLogger(RequestUtil.class); + private static final Logger log = LoggerFactory.getLogger(RequestUtil.class); private RequestUtil() { } @@ -197,7 +200,7 @@ public class RequestUtil { public static String getText(HttpServletRequest request) throws IOException { - return IOUtils.toString(request.getInputStream(), UTF8); + return IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); } /** @@ -237,6 +240,44 @@ public class RequestUtil { return ipAddress; } + /** + * 获取客户端真实ip + * @param request request + * @return 返回ip + */ + public static String getIP(ServerHttpRequest request) { + HttpHeaders headers = request.getHeaders(); + String ipAddress = headers.getFirst("x-forwarded-for"); + if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = headers.getFirst("Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = headers.getFirst("WL-Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) { + ipAddress = Optional.ofNullable(request.getRemoteAddress()) + .map(address -> address.getAddress().getHostAddress()) + .orElse(""); + if (IP_LOCAL.equals(ipAddress)) { + // 根据网卡取本机配置的IP + try { + InetAddress inet = InetAddress.getLocalHost(); + ipAddress = inet.getHostAddress(); + } catch (UnknownHostException e) { + // ignore + } + } + } + + // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ipAddress != null && ipAddress.length() > IP_LEN) { + int index = ipAddress.indexOf(","); + if (index > 0) { + ipAddress = ipAddress.substring(0, index); + } + } + return ipAddress; + } /** * 获取上传文件内容 diff --git a/sop-common/sop-service-common/pom.xml b/sop-common/sop-service-common/pom.xml index e32c138e..a9c89991 100644 --- a/sop-common/sop-service-common/pom.xml +++ b/sop-common/sop-service-common/pom.xml @@ -42,6 +42,11 @@ + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + true + org.springframework spring-webmvc @@ -97,6 +102,7 @@ javax.servlet-api provided + diff --git a/sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServiceConfiguration.java b/sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServiceConfiguration.java index 84534a9d..87c26fde 100644 --- a/sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServiceConfiguration.java +++ b/sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/configuration/BaseServiceConfiguration.java @@ -1,5 +1,7 @@ package com.gitee.sop.servercommon.configuration; +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import com.alibaba.cloud.nacos.discovery.NacosWatch; import com.gitee.sop.servercommon.bean.ServiceConfig; import com.gitee.sop.servercommon.interceptor.ServiceContextInterceptor; import com.gitee.sop.servercommon.manager.EnvironmentContext; @@ -7,13 +9,16 @@ import com.gitee.sop.servercommon.manager.ServiceRouteController; import com.gitee.sop.servercommon.mapping.ApiMappingHandlerMapping; import com.gitee.sop.servercommon.message.ServiceErrorFactory; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.scheduling.TaskScheduler; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -21,6 +26,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.util.List; /** @@ -84,6 +90,15 @@ public class BaseServiceConfiguration implements WebMvcConfigurer, WebMvcRegistr return new ServiceRouteController(); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty("spring.cloud.nacos.discovery.server-addr") + public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties, ObjectProvider taskScheduler) { + // 在元数据中新增启动时间,不能修改这个值,不然网关拉取接口会有问题 + nacosDiscoveryProperties.getMetadata().put("time.startup", String.valueOf(System.currentTimeMillis())); + return new NacosWatch(nacosDiscoveryProperties, taskScheduler); + } + @PostConstruct public final void after() { log.info("-----spring容器加载完毕-----"); diff --git a/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/DemoOrderController.java b/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/DemoOrderController.java new file mode 100644 index 00000000..4dab0d29 --- /dev/null +++ b/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/DemoOrderController.java @@ -0,0 +1,74 @@ +package com.gitee.sop.storyweb.controller; + +import com.alibaba.fastjson.JSON; +import com.gitee.sop.servercommon.annotation.ApiMapping; +import com.gitee.sop.servercommon.util.UploadUtil; +import lombok.Data; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +/** + * @author tanghc + */ +@RestController +public class DemoOrderController { + + @Value("${server.port}") + private String port; + + @ApiMapping("member.info.get") + public Object member(MemberInfoGetParam param, HttpServletRequest request) { + if ("tom".equals(param.name)) { + throw new IllegalArgumentException("name参数错误"); + } + Collection uploadFiles = UploadUtil.getUploadFiles(request); + for (MultipartFile uploadFile : uploadFiles) { + try { + System.out.println("文件名称:" + uploadFile.getOriginalFilename() + + " 表单名称:" + uploadFile.getName() + + " 文件内容:" + + IOUtils.toString(uploadFile.getInputStream(), StandardCharsets.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + } + } + System.out.println(param); + return JSON.parseObject("{\n" + + " \"id\": 123,\n" + + " \"name\": \"jim\",\n" + + " \"member_info\": {\n" + + " \"is_vip\": 1,\n" + + " \"vip_endtime\": \"2020-11-11 11:11:11\"\n" + + " }\n" + + "}", MemberInfoGetResult.class); + } + + + @Data + public static class MemberInfoGetParam { + private String name; + private Integer age; + private String address; + } + + @Data + public static class MemberInfoGetResult { + private Integer id; + private String name; + private MemberInfoGetResultMemberInfo member_info; + } + + @Data + public static class MemberInfoGetResultMemberInfo { + private Byte is_vip; + private String vip_endtime; + } + +} diff --git a/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/TokenController.java b/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/TokenController.java index f0da8211..ab28ff9b 100644 --- a/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/TokenController.java +++ b/sop-example/sop-story/src/main/java/com/gitee/sop/storyweb/controller/TokenController.java @@ -6,6 +6,7 @@ import com.gitee.sop.servercommon.bean.ServiceContext; import com.gitee.sop.storyweb.controller.param.StoryParam; import com.gitee.sop.storyweb.controller.result.StoryResult; import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RestController; @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController; @Api(tags = "故事接口") public class TokenController { + @ApiOperation(value="传递token", notes = "传递token") @ApiMapping(value = "story.token.get", needToken = true/* 设置true,网关会校验token是否存在 */) public StoryResult token(StoryParam story) { OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext(); diff --git a/sop-gateway/src/main/java/com/gitee/sop/gateway/interceptor/MyRouteInterceptor.java b/sop-gateway/src/main/java/com/gitee/sop/gateway/interceptor/MyRouteInterceptor.java index 91039ac8..0a3b8884 100644 --- a/sop-gateway/src/main/java/com/gitee/sop/gateway/interceptor/MyRouteInterceptor.java +++ b/sop-gateway/src/main/java/com/gitee/sop/gateway/interceptor/MyRouteInterceptor.java @@ -3,6 +3,7 @@ package com.gitee.sop.gateway.interceptor; import com.gitee.sop.gatewaycommon.interceptor.RouteInterceptor; import com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext; import com.gitee.sop.gatewaycommon.param.ApiParam; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.stereotype.Component; /** @@ -21,7 +22,9 @@ public class MyRouteInterceptor implements RouteInterceptor { @Override public void afterRoute(RouteInterceptorContext context) { - System.out.println("请求成功,微服务返回结果:" + context.getServiceResult()); + ServiceInstance serviceInstance = context.getServiceInstance(); + System.out.println("请求成功,serviceId:" + serviceInstance.getServiceId() + "(" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + ")" + + ",微服务返回结果:" + context.getServiceResult()); } @Override diff --git a/sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbRouteConfigManager.java b/sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbRouteConfigManager.java index 475addd0..0108cffd 100644 --- a/sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbRouteConfigManager.java +++ b/sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbRouteConfigManager.java @@ -1,6 +1,5 @@ package com.gitee.sop.gateway.manager; -import com.gitee.sop.gateway.mapper.ConfigRouteBaseMapper; import com.gitee.sop.gateway.mapper.ConfigRouteLimitMapper; import com.gitee.sop.gateway.mapper.ConfigRouteMapper; import com.gitee.sop.gatewaycommon.bean.ChannelMsg; @@ -21,9 +20,6 @@ import java.util.List; @Service public class DbRouteConfigManager extends DefaultRouteConfigManager { - @Autowired - ConfigRouteBaseMapper configRouteBaseMapper; - @Autowired ConfigRouteLimitMapper configRouteLimitMapper; diff --git a/sop-sdk/sdk-go/common/HttpTool.go b/sop-sdk/sdk-go/common/HttpTool.go new file mode 100644 index 00000000..056e7d67 --- /dev/null +++ b/sop-sdk/sdk-go/common/HttpTool.go @@ -0,0 +1,127 @@ +package common + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +type UploadFile struct { + // 表单名称 + Name string + // 文件全路径 + Filepath string +} + +// 请求客户端 +var httpClient = &http.Client{} + +func Get(reqUrl string, reqParams map[string]string, headers map[string]string) string { + urlParams := url.Values{} + Url, _ := url.Parse(reqUrl) + for key, val := range reqParams { + urlParams.Set(key, val) + } + + //如果参数中有中文参数,这个方法会进行URLEncode + Url.RawQuery = urlParams.Encode() + // 得到完整的url,http://xx?query + urlPath := Url.String() + + httpRequest,_ := http.NewRequest("GET", urlPath, nil) + // 添加请求头 + if headers != nil { + for k, v := range headers { + httpRequest.Header.Add(k,v) + } + } + // 发送请求 + resp, err := httpClient.Do(httpRequest) + if err != nil { + panic(err) + } + defer resp.Body.Close() + response, _ := ioutil.ReadAll(resp.Body) + return string(response) +} + +func PostForm(reqUrl string, reqParams map[string]string, headers map[string]string) string { + return post(reqUrl, reqParams, "application/x-www-form-urlencoded", nil, headers) +} + +func PostJson(reqUrl string, reqParams map[string]string, headers map[string]string) string { + return post(reqUrl, reqParams, "application/json", nil, headers) +} + +func PostFile(reqUrl string, reqParams map[string]string, files []UploadFile, headers map[string]string) string { + return post(reqUrl, reqParams, "multipart/form-data", files, headers) +} + + +func post(reqUrl string, reqParams map[string]string, contentType string, files []UploadFile, headers map[string]string) string { + requestBody, realContentType := getReader(reqParams, contentType, files) + httpRequest,_ := http.NewRequest("POST", reqUrl, requestBody) + // 添加请求头 + httpRequest.Header.Add("Content-Type", realContentType) + if headers != nil { + for k, v := range headers { + httpRequest.Header.Add(k,v) + } + } + // 发送请求 + resp, err := httpClient.Do(httpRequest) + if err != nil { + panic(err) + } + defer resp.Body.Close() + response, _ := ioutil.ReadAll(resp.Body) + return string(response) +} + +func getReader(reqParams map[string]string, contentType string, files []UploadFile) (io.Reader, string) { + if strings.Index(contentType, "json") > -1 { + bytesData, _ := json.Marshal(reqParams) + return bytes.NewReader(bytesData), contentType + } else if files != nil { + body := &bytes.Buffer{} + // 文件写入 body + writer := multipart.NewWriter(body) + for _, uploadFile := range files { + file, err := os.Open(uploadFile.Filepath) + if err != nil { + panic(err) + } + part, err := writer.CreateFormFile(uploadFile.Name, filepath.Base(uploadFile.Filepath)) + if err != nil { + panic(err) + } + _, err = io.Copy(part, file) + file.Close() + } + // 其他参数列表写入 body + for k, v := range reqParams { + if err := writer.WriteField(k, v); err != nil { + panic(err) + } + } + if err := writer.Close(); err != nil { + panic(err) + } + // 上传文件需要自己专用的contentType + return body, writer.FormDataContentType() + } else { + urlValues := url.Values{} + for key, val := range reqParams { + urlValues.Set(key, val) + } + reqBody:= urlValues.Encode() + return strings.NewReader(reqBody), contentType + } +} diff --git a/sop-sdk/sdk-go/common/IRequest.go b/sop-sdk/sdk-go/common/IRequest.go new file mode 100644 index 00000000..ef0f10e7 --- /dev/null +++ b/sop-sdk/sdk-go/common/IRequest.go @@ -0,0 +1,24 @@ +package common + +type RequestType string + +const ( + GET RequestType = "GET" + POST_JSON RequestType = "POST_JSON" + POST_FORM RequestType = "POST_FORM" + POST_UPLOAD RequestType = "POST_UPLOAD" +) + +type Model struct { + // 业务参数 + BizModel interface{} + // 上传文件 + Files []UploadFile +} + +type IRequest interface { + GetMethod() string + GetVersion() string + GetRequestType() RequestType + GetModel() Model +} diff --git a/sop-sdk/sdk-go/common/OpenClient.go b/sop-sdk/sdk-go/common/OpenClient.go new file mode 100644 index 00000000..23289c52 --- /dev/null +++ b/sop-sdk/sdk-go/common/OpenClient.go @@ -0,0 +1,108 @@ +package common + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + "time" +) + +var headers = map[string]string{ + "Accept-Encoding": "identity", +} + +type IClient interface { + Execute() string +} + +type OpenClient struct { + AppId string + PrivateKey string + Url string +} + +func (client OpenClient) ExecuteToken(iRequest IRequest, token string) []byte { + model := iRequest.GetModel() + bizModel := model.BizModel + types := reflect.TypeOf(bizModel) + values := reflect.ValueOf(bizModel) + params := make(map[string]interface{}) + //遍历结构体的所有字段 + for i := 0; i < values.NumField(); i++ { + // 获取到struct标签,需要通过reflect.Type来获取tag标签的值 + fieldName := types.Field(i).Tag.Get("json") + // 如果该字段有tag标签就显示,否则就不显示 + if fieldName != "" { + params[fieldName] = values.Field(i).Interface() + } + } + + requestType := iRequest.GetRequestType() + var response string + allParams := client.buildParams(iRequest, params, token) + if model.Files != nil && len(model.Files) > 0 { + response = PostFile(client.Url, allParams, model.Files, headers) + } else { + switch requestType { + case GET: + response = Get(client.Url, allParams, headers) + case POST_FORM: + response = PostForm(client.Url, allParams, headers) + case POST_JSON: + response = PostJson(client.Url, allParams, headers) + case POST_UPLOAD: + response = PostFile(client.Url, allParams, model.Files, headers) + default: + panic(errors.New("GetRequestType()返回错误")) + } + } + return parseResponseResult(iRequest, response) +} + +func parseResponseResult(iRequest IRequest, response string) []byte { + var responseRoot = map[string]interface{}{} + var err = json.Unmarshal([]byte(response), &responseRoot) + if err != nil { + panic(err) + } + requestId := responseRoot["request_id"].(string) + var responseDataMap = responseRoot["error_response"] + if responseDataMap == nil { + dataName := strings.ReplaceAll(iRequest.GetMethod(), ".", "_") + "_response" + responseDataMap = responseRoot[dataName] + } + responseDataMap.(map[string]interface{})["request_id"] = requestId + // json数据 + dataJsonBytes, _ := json.Marshal(responseDataMap) + return dataJsonBytes +} + +func (client OpenClient) buildParams(iRequest IRequest, params map[string]interface{}, token string) map[string]string { + allParams := map[string]string{ + "app_id": client.AppId, + "method": iRequest.GetMethod(), + "charset": "UTF-8", + "sign_type": "RSA2", + "timestamp": time.Now().Format("2006-01-02 15:04:05"), + "version": iRequest.GetVersion(), + } + + if token != "" { + allParams["access_token"] = token + } + + // 添加业务参数 + for k, v := range params { + allParams[k] = ToString(v) + } + + // 构建sign + sign := CreateSign(allParams, client.PrivateKey, "RSA2") + allParams["sign"] = sign + return allParams +} + +func (client OpenClient) Execute(iRequest IRequest) []byte { + return client.ExecuteToken(iRequest, "") +} diff --git a/sop-sdk/sdk-go/common/SignUtil.go b/sop-sdk/sdk-go/common/SignUtil.go new file mode 100644 index 00000000..b3669bea --- /dev/null +++ b/sop-sdk/sdk-go/common/SignUtil.go @@ -0,0 +1,99 @@ +package common + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "sort" + "strings" +) + +const ( + PEM_BEGIN = "-----BEGIN RSA PRIVATE KEY-----\n" + PEM_END = "\n-----END RSA PRIVATE KEY-----" +) + +func CreateSign(allParams map[string]string, privateKey string, signType string) string { + signContent := GetSignContent(allParams) + return Sign(signContent, privateKey, signType) +} + +func Sign(signContent string, privateKey string, signType string) string { + if signType == "RSA" { + return RsaSign(signContent, privateKey, crypto.SHA1) + } else if signType == "RSA2" { + return RsaSign(signContent, privateKey, crypto.SHA256) + } else { + panic(errors.New("signType错误")) + } +} + +func RsaSign(signContent string, privateKey string, hash crypto.Hash) string { + shaNew := hash.New() + shaNew.Write([]byte(signContent)) + hashed := shaNew.Sum(nil) + priKey, err := ParsePrivateKey(privateKey) + if err != nil { + panic(err) + } + + signature, err := rsa.SignPKCS1v15(rand.Reader, priKey, hash, hashed) + if err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(signature) +} + +func ParsePrivateKey(privateKey string)(*rsa.PrivateKey, error) { + privateKey = FormatPrivateKey(privateKey) + // 2、解码私钥字节,生成加密对象 + block, _ := pem.Decode([]byte(privateKey)) + if block == nil { + return nil, errors.New("私钥信息错误!") + } + // 3、解析DER编码的私钥,生成私钥对象 + priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return priKey, nil +} + +func FormatPrivateKey(privateKey string) string { + if !strings.HasPrefix(privateKey, PEM_BEGIN) { + privateKey = PEM_BEGIN + privateKey + } + if !strings.HasSuffix(privateKey, PEM_END) { + privateKey = privateKey + PEM_END + } + return privateKey +} + + +/** +1.筛选并排序 +获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数,并按照参数名ASCII码递增排序(字母升序排序), +如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。 + +2.拼接 +将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。 + */ +func GetSignContent(allParams map[string]string) string { + keys := make([]string, 0, len(allParams)) + var result []string + for k := range allParams { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + val := allParams[key] + if len(val) > 0 { + result = append(result, key + "=" + val) + } + } + return strings.Join(result, "&") +} \ No newline at end of file diff --git a/sop-sdk/sdk-go/common/StringUtil.go b/sop-sdk/sdk-go/common/StringUtil.go new file mode 100644 index 00000000..e4a49c56 --- /dev/null +++ b/sop-sdk/sdk-go/common/StringUtil.go @@ -0,0 +1,56 @@ +package common + +import ( + "encoding/json" + "strconv" +) + +func ToString(value interface{}) string { + var key string + switch value.(type) { + case float64: + ft := value.(float64) + key = strconv.FormatFloat(ft, 'f', -1, 64) + case float32: + ft := value.(float32) + key = strconv.FormatFloat(float64(ft), 'f', -1, 64) + case int: + it := value.(int) + key = strconv.Itoa(it) + case uint: + it := value.(uint) + key = strconv.Itoa(int(it)) + case int8: + it := value.(int8) + key = strconv.Itoa(int(it)) + case uint8: + it := value.(uint8) + key = strconv.Itoa(int(it)) + case int16: + it := value.(int16) + key = strconv.Itoa(int(it)) + case uint16: + it := value.(uint16) + key = strconv.Itoa(int(it)) + case int32: + it := value.(int32) + key = strconv.Itoa(int(it)) + case uint32: + it := value.(uint32) + key = strconv.Itoa(int(it)) + case int64: + it := value.(int64) + key = strconv.FormatInt(it, 10) + case uint64: + it := value.(uint64) + key = strconv.FormatUint(it, 10) + case string: + key = value.(string) + case []byte: + key = string(value.([]byte)) + default: + newValue, _ := json.Marshal(value) + key = string(newValue) + } + return key +} \ No newline at end of file diff --git a/sop-sdk/sdk-go/model/MemberInfoGetModel.go b/sop-sdk/sdk-go/model/MemberInfoGetModel.go new file mode 100644 index 00000000..06ddbd4d --- /dev/null +++ b/sop-sdk/sdk-go/model/MemberInfoGetModel.go @@ -0,0 +1,7 @@ +package model + +type MemberInfoGetModel struct { + Name string `json:"name"` + Age uint32 `json:"age"` + Address string `json:"address"` +} diff --git a/sop-sdk/sdk-go/readme.md b/sop-sdk/sdk-go/readme.md new file mode 100644 index 00000000..0d914e11 --- /dev/null +++ b/sop-sdk/sdk-go/readme.md @@ -0,0 +1,43 @@ +# sdk-go + +```go + +// 应用ID +const appId string = "xx" +// 应用私钥 +const privateKey string = "xx" +// 请求地址 +const url string = "http://localhost:7071/prod/gw68uy85" + +// 请求客户端 +var openClient = common.OpenClient{AppId: appId, PrivateKey: privateKey, Url: url} + +func main() { + // 创建请求 + memberInfoGetRequest := request.MemberInfoGetRequest{} + // 请求参数 + memberInfoGetRequest.BizModel = model.MemberInfoGetModel{Name: "jim", Age: 22, Address: "xx"} + + // 添加上传文件 + //path, _ := os.Getwd() + //files := []common.UploadFile{ + // {Name:"file1", Filepath:path + "/test/aa.txt"}, + // {Name:"file2", Filepath:path + "/test/bb.txt"}, + //} + //memberInfoGetRequest.Files = files + + // 发送请求,返回json bytes + var jsonBytes = openClient.Execute(memberInfoGetRequest) + fmt.Printf("data:%s\n", string(jsonBytes)) + // 转换结果 + var memberInfoGetResponse response.MemberInfoGetResponse + response.ConvertResponse(jsonBytes, &memberInfoGetResponse) + + if memberInfoGetResponse.IsSuccess() { + fmt.Printf("is_vip:%d, vip_endtime:%s\n", memberInfoGetResponse.MemberInfo.IsVip, memberInfoGetResponse.MemberInfo.VipEndtime) + } else { + fmt.Printf("code:%s, msg:%s, subCode:%s, subMsg:%s\n", + memberInfoGetResponse.Code, memberInfoGetResponse.Msg, memberInfoGetResponse.SubCode, memberInfoGetResponse.SubMsg) + } +} +``` \ No newline at end of file diff --git a/sop-sdk/sdk-go/request/MemberInfoGetRequest.go b/sop-sdk/sdk-go/request/MemberInfoGetRequest.go new file mode 100644 index 00000000..1cba4765 --- /dev/null +++ b/sop-sdk/sdk-go/request/MemberInfoGetRequest.go @@ -0,0 +1,23 @@ +package request + +import "../common" + +type MemberInfoGetRequest struct { + common.Model +} + +func (MemberInfoGetRequest) GetMethod() string { + return "member.info.get" +} + +func (MemberInfoGetRequest) GetVersion() string { + return "1.0" +} + +func (MemberInfoGetRequest) GetRequestType() common.RequestType { + return common.GET +} + +func (req MemberInfoGetRequest) GetModel() common.Model { + return req.Model +} diff --git a/sop-sdk/sdk-go/response/BaseResponse.go b/sop-sdk/sdk-go/response/BaseResponse.go new file mode 100644 index 00000000..bda5b78d --- /dev/null +++ b/sop-sdk/sdk-go/response/BaseResponse.go @@ -0,0 +1,26 @@ +package response + +import "encoding/json" + +type IResponse interface { + IsSuccess() bool +} + +type BaseResponse struct { + RequestId string `json:"request_id"` + Code string `json:"code"` + Msg string `json:"msg"` + SubCode string `json:"sub_code"` + SubMsg string `json:"sub_msg"` +} + +func (resp BaseResponse) IsSuccess() bool { + return len(resp.SubCode) == 0 +} + +func ConvertResponse(data []byte, ptr interface{}) { + err := json.Unmarshal(data, ptr) + if err != nil { + panic(err) + } +} diff --git a/sop-sdk/sdk-go/response/MemberInfoGetResponse.go b/sop-sdk/sdk-go/response/MemberInfoGetResponse.go new file mode 100644 index 00000000..b566196f --- /dev/null +++ b/sop-sdk/sdk-go/response/MemberInfoGetResponse.go @@ -0,0 +1,14 @@ +package response + +type MemberInfoGetResponse struct { + BaseResponse + + Id int32 `json:"id"` + Name string `json:"name"` + MemberInfo MemberInfo `json:"member_info"` +} + +type MemberInfo struct { + IsVip int32 `json:"is_vip"` + VipEndtime string `json:"vip_endtime"` +} diff --git a/sop-sdk/sdk-go/test/aa.txt b/sop-sdk/sdk-go/test/aa.txt new file mode 100644 index 00000000..524527cb --- /dev/null +++ b/sop-sdk/sdk-go/test/aa.txt @@ -0,0 +1 @@ +hello你好123 \ No newline at end of file diff --git a/sop-sdk/sdk-go/test/bb.txt b/sop-sdk/sdk-go/test/bb.txt new file mode 100644 index 00000000..85f8e329 --- /dev/null +++ b/sop-sdk/sdk-go/test/bb.txt @@ -0,0 +1 @@ +文件bb的内容 \ No newline at end of file diff --git a/sop-sdk/sdk-go/test/test.go b/sop-sdk/sdk-go/test/test.go new file mode 100644 index 00000000..27b2b919 --- /dev/null +++ b/sop-sdk/sdk-go/test/test.go @@ -0,0 +1,48 @@ +package main + +import ( + "../common" + "../model" + "../request" + "../response" + "fmt" +) + +// 应用ID +const appId string = "201904035630907729292csharp" +// 应用私钥 +const privateKey string = "MIIEowIBAAKCAQEA5+OvJxeSzf44NxQ/cl7Ii+BzPg2k6sRcvH4ffOtU5Dzq1/oEvg02nxIhmwOHBZmjbmuUu0aLsfglUTAwqfXftfAKZidshsgj9NNh0/kxk0avRZ1UoljWGz/FxVZA0ogbxxhohPZ9jWcD+eBQcIwF2DtHfAJqWWZrYFnCMeHD8mPzxo2kwXSvDzi0vf9I2tKiYvNG26a9FqeYtPOoi81sdS3+70HOMdxP8ejXtyfnKpKz7Dx506LCIRS5moWS3Q5eTLV3NGX/1CSJ8wpQA2DAQTjVhX5eVu7Yqz12t8W+sjWM/tHUR6cgwYYR10p7tSCeCPzkigjGxKm4cYXWtATQJQIDAQABAoIBAHFDsgrrJca+NKEan77ycwx3jnKx4WrWjOF4zVKL9AQjiSYDNgvKknJyPb3kpC/lEoHdxGERHSzJoxib7DkoIqRQYhPxj73pxj5QfYk3P7LLJNNg/LTrpXDb3nL8JV9wIflGf87qQvstZTDJEyFWE4jBs7Hr0BxovWvri8InnzkmERJ1cbGJgNHe1Y3Zo2tw0yaHxQCxLuajP+notRZhD9bEp7uKeI0w9AvlW6k8m/7y10F0BK/TlyW8rQiEC391yOiRYoMcUh4hd2Q9bMx3jngZgX8PXIvZZcup4/pvWlv1alwhB2tsnLdazP62r1MO80vLyLunzGO+7WwCjEYlVaECgYEA+lQRFmbhKaPuAuXMtY31Fbga8nedka5TjnEV7+/kX+yowE2OlNujF+ZG8UTddTxAGv56yVNi/mjRlgD74j8z0eOsgvOq9mwbCrgLhLo51H9O/wAxtb+hBKtC5l50pBr4gER6d8W6EQNTSGojnMIaLXTkAZ5Qf6Z8e2HFVdOn0X0CgYEA7SSrTokwzukt5KldNu5ukyyd+C3D1i6orbg6qD73EP9CfNMfGSBn7dDv9wMSJH01+Ty+RgTROgtjGRDbMJWnfbdt/61NePr9ar5sb6Nbsf7/I0w7cZF5dsaFYgzaOfQYquzXPbLQHkpMT64bqpv/Mwy4F2lFvaYWY5fA4pC2uckCgYEAg75Ym9ybJaoTqky8ttQ2Jy8UZ4VSVQhVC0My02sCWwWXLlXi8y7An+Rec73Ve0yxREOn5WrQT6pkmzh7V/ABWrYi5WxODpCIjtSbo0fLBa3Wqle00b0/hdCITetqIa/cFs1zUrOqICgK3bKWeXqiAkhhcwSZwwSgwOKM04Wn7ZUCgYBvhHX2mbdVJfyJ8kc+hMOE/E9RHRxiBVEXWHJlGi8PVCqNDq8qHr4g7Mdbzprig+s0yKblwHAvrpkseWvKHiZEjVTyDipHgShY4TGXEigVvUd37uppTrLi8xpYcJjS9gH/px7VCdiq1d+q/MJP6coJ1KphgATm2UrgDMYNBWaYWQKBgEHRxrmER7btUF60/YgcqPHFc8RpYQB2ZZE0kyKGDqk2Data1XYUY6vsPAU28yRLAaWr/D2H17iyLkxP80VLm6QhifxCadv90Q/Wl1DFfOJQMW6avyQ0so6G0wFq/LJxaFK4iLXQn1RJnmTp6BYiJMmK2BhFbRzw8ssMoF6ad2rr" +// 请求路径 +const url string = "http://localhost:8081" + +// 请求客户端 +var openClient = common.OpenClient{AppId: appId, PrivateKey: privateKey, Url: url} + +func main() { + // 创建请求 + memberInfoGetRequest := request.MemberInfoGetRequest{} + // 设置请求参数 + memberInfoGetRequest.BizModel = model.MemberInfoGetModel{Name: "jim", Age: 22, Address: "xx"} + + // 添加上传文件 + //path, _ := os.Getwd() + //files := []common.UploadFile{ + // {Name:"file1", Filepath:path + "/test/aa.txt"}, + // {Name:"file2", Filepath:path + "/test/bb.txt"}, + //} + //memberInfoGetRequest.Files = files + + // 发送请求,返回json bytes + var jsonBytes = openClient.Execute(memberInfoGetRequest) + fmt.Printf("data:%s\n", string(jsonBytes)) + // 转换结果 + var memberInfoGetResponse response.MemberInfoGetResponse + response.ConvertResponse(jsonBytes, &memberInfoGetResponse) + + if memberInfoGetResponse.IsSuccess() { + fmt.Printf("is_vip:%d, vip_endtime:%s\n", memberInfoGetResponse.MemberInfo.IsVip, memberInfoGetResponse.MemberInfo.VipEndtime) + } else { + fmt.Printf("code:%s, msg:%s, subCode:%s, subMsg:%s\n", + memberInfoGetResponse.Code, memberInfoGetResponse.Msg, memberInfoGetResponse.SubCode, memberInfoGetResponse.SubMsg) + } +} diff --git a/sop-sdk/sdk-python/.gitignore b/sop-sdk/sdk-python/.gitignore new file mode 100644 index 00000000..297111f9 --- /dev/null +++ b/sop-sdk/sdk-python/.gitignore @@ -0,0 +1,27 @@ +/target/ +/venv/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + diff --git a/sop-sdk/sdk-python/common/JsonUtil.py b/sop-sdk/sdk-python/common/JsonUtil.py new file mode 100644 index 00000000..73e316cd --- /dev/null +++ b/sop-sdk/sdk-python/common/JsonUtil.py @@ -0,0 +1,21 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import json + + +def to_json_string(obj): + """将对象转换成json字符串 + + :param obj: 对象 + :type obj: object + + :return: 返回json + :rtype: str + """ + if isinstance(obj, dict): + param = obj + else: + param = obj.__dict__ + return json.dumps(param, ensure_ascii=False) + diff --git a/sop-sdk/sdk-python/common/OpenClient.py b/sop-sdk/sdk-python/common/OpenClient.py new file mode 100644 index 00000000..821c5f3e --- /dev/null +++ b/sop-sdk/sdk-python/common/OpenClient.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import json +import time + +import requests + +from common import SignUtil, RequestTypes +from common.RequestType import RequestType + +_headers = {'Accept-Encoding': 'identity'} + + +class OpenClient: + """调用客户端""" + __app_id = '' + __private_key = '' + __url = '' + + def __init__(self, app_id, private_key, url): + """客户端 + + :param app_id: 应用ID + :type app_id: str + + :param private_key: 应用私钥 + :type private_key: str + + :param url: 请求URL + :type url: str + """ + self.__app_id = app_id + self.__private_key = private_key + self.__url = url + + def execute(self, request, token=None): + """ + + :param request: 请求对象,BaseRequest的子类 + + :param token: (Optional) token + :type token: str + + :return: 返回请求结果 + :rtype: BaseResponse + """ + biz_model = request.biz_model + request_type = request.get_request_type() + if not isinstance(request_type, RequestType): + raise Exception('get_request_type返回错误类型,正确方式:RequestTypes.XX') + + params = biz_model.__dict__ + if request.files is not None: + response = self._post_file(request, params, token) + elif request_type == RequestTypes.GET: + response = self._get(request, params, token) + elif request_type == RequestTypes.POST_FORM: + response = self._post_form(request, params, token) + elif request_type == RequestTypes.POST_JSON: + response = self._post_json(request, params, token) + elif request_type == RequestTypes.POST_UPLOAD: + response = self._post_file(request, params, token) + else: + raise Exception('get_request_type设置错误') + + return self._parse_response(response, request) + + def _get(self, request, params, token): + all_params = self._build_params(request, params, token) + return requests.get(self.__url, all_params, headers=_headers).text + + def _post_form(self, request, params, token): + all_params = self._build_params(request, params, token) + return requests.post(self.__url, data=all_params, headers=_headers).text + + def _post_json(self, request, params, token): + all_params = self._build_params(request, params, token) + return requests.post(self.__url, json=all_params, headers=_headers).text + + def _post_file(self, request, params, token): + all_params = self._build_params(request, params, token) + return requests.request('POST', self.__url, data=all_params, files=request.files, headers=_headers).text + + def _build_params(self, request, params, token): + """构建所有的请求参数 + + :param request: 请求对象 + :type request: request.BaseRequest + + :param params: 业务请求参数 + :type params: dict + + :param token: token + :type token: str + + :return: 返回请求参数 + :rtype: str + """ + all_params = { + 'app_id': self.__app_id, + 'method': request.get_method(), + 'charset': 'UTF-8', + 'sign_type': 'RSA2', + 'timestamp': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), + 'version': request.get_version() + } + + if token is not None: + all_params['access_token'] = token + + # 添加业务参数 + all_params.update(params) + + # 构建sign + sign = SignUtil.create_sign(all_params, self.__private_key, 'RSA2') + all_params['sign'] = sign + return all_params + + def _parse_response(self, resp, request): + response_dict = json.loads(resp) + return request.parse_response(response_dict) diff --git a/sop-sdk/sdk-python/common/RequestType.py b/sop-sdk/sdk-python/common/RequestType.py new file mode 100644 index 00000000..4df03044 --- /dev/null +++ b/sop-sdk/sdk-python/common/RequestType.py @@ -0,0 +1,8 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + + +class RequestType: + + def __init__(self): + pass diff --git a/sop-sdk/sdk-python/common/RequestTypes.py b/sop-sdk/sdk-python/common/RequestTypes.py new file mode 100644 index 00000000..edf08442 --- /dev/null +++ b/sop-sdk/sdk-python/common/RequestTypes.py @@ -0,0 +1,6 @@ +from common.RequestType import RequestType + +GET = RequestType() +POST_JSON = RequestType() +POST_FORM = RequestType() +POST_UPLOAD = RequestType() diff --git a/sop-sdk/sdk-python/common/SignUtil.py b/sop-sdk/sdk-python/common/SignUtil.py new file mode 100644 index 00000000..7b152055 --- /dev/null +++ b/sop-sdk/sdk-python/common/SignUtil.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import rsa +import base64 + +__pem_begin = '-----BEGIN RSA PRIVATE KEY-----\n' +__pem_end = '\n-----END RSA PRIVATE KEY-----' + + +def create_sign(all_params, private_key, sign_type): + """创建签名 + + :param all_params: 参数 + :type all_params: dict + + :param private_key: 私钥字符串 + :type private_key: str + + :param sign_type: 签名类型,'RSA', 'RSA2'二选一 + :type sign_type: str + + :return: 返回签名内容 + :rtype: str + """ + sign_content = get_sign_content(all_params) + private_key = _format_private_key(private_key) + return sign(sign_content, private_key, sign_type) + + +def _format_private_key(private_key): + if not private_key.startswith(__pem_begin): + private_key = __pem_begin + private_key + if not private_key.endswith(__pem_end): + private_key = private_key + __pem_end + return private_key + + +def get_sign_content(params): + """构建签名内容 + + 1.筛选并排序 + 获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数,并按照参数名ASCII码递增排序(字母升序排序), + 如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。 + + 2.拼接 + 将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。 + + :param params: 参数 + :type params: dict + + :return: 返回签名内容 + :rtype: str + """ + keys = params.keys() + keys.sort() + result = [] + for key in keys: + value = str(params.get(key)) + if len(value) > 0: + result.append(key + '=' + value) + + return '&'.join(result) + + +def sign(content, private_key, sign_type): + """签名 + + :param content: 签名内容 + :type content: str + + :param private_key: 私钥字符串 + :type private_key: str + + :param sign_type: 签名类型,'RSA', 'RSA2'二选一 + :type sign_type: str + + :return: 返回签名内容 + :rtype: str + """ + if sign_type.upper() == 'RSA': + return rsa_sign(content, private_key, 'SHA-1') + elif sign_type.upper() == 'RSA2': + return rsa_sign(content, private_key, 'SHA-256') + else: + raise Exception('sign_type错误') + + +def rsa_sign(content, private_key, _hash): + """SHAWithRSA + + :param content: 签名内容 + :type content: str + + :param private_key: 私钥 + :type private_key: str + + :param _hash: hash算法,如:SHA-1,SHA-256 + :type _hash: str + + :return: 签名内容 + :rtype: str + """ + pri_key = rsa.PrivateKey.load_pkcs1(private_key.encode('utf-8')) + sign_result = rsa.sign(content, pri_key, _hash) + return base64.b64encode(sign_result) diff --git a/sop-sdk/sdk-python/common/__init__.py b/sop-sdk/sdk-python/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sop-sdk/sdk-python/model/MemberInfoGetModel.py b/sop-sdk/sdk-python/model/MemberInfoGetModel.py new file mode 100644 index 00000000..beef1ec8 --- /dev/null +++ b/sop-sdk/sdk-python/model/MemberInfoGetModel.py @@ -0,0 +1,7 @@ + +class MemberInfoGetModel: + """MemberInfoGetModel""" + + name = None + age = None + address = None diff --git a/sop-sdk/sdk-python/model/__init__.py b/sop-sdk/sdk-python/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sop-sdk/sdk-python/readme.md b/sop-sdk/sdk-python/readme.md new file mode 100644 index 00000000..3ac5e61a --- /dev/null +++ b/sop-sdk/sdk-python/readme.md @@ -0,0 +1,56 @@ +# sdk-python + +安装 + +`pip install requests` + +`pip install rsa` + +- 调用方式 + +```python + # 创建请求 + request = MemberInfoGetRequest() + # 请求参数 + model = MemberInfoGetModel() + model.age = 22 + model.name = 'jim' + model.address = 'xx' + # 添加请求参数 + request.biz_model = model + + # 添加上传文件 + # files = { + # 'file1': open('aa.txt', 'rb'), + # 'file2': open('bb.txt', 'rb') + # } + # request.files = files + + # 调用请求 + response = self.client.execute(request) + + if response.is_success(): + print 'response: ', response + print 'is_vip:', response.get('member_info').get('is_vip', 0) + else: + print '请求失败,code:%s, msg:%s, sub_code:%s, sub_msg:%s' % \ + (response.code, response.msg, response.sub_code, response.sub_msg) +``` + +详见`test.py` + +代码规范: + +| Type | Public | Internal | +| -------------------------- | ------------------ | ----------------------------------------------------------------- | +| Modules | lower_with_under | _lower_with_under | +| Packages | lower_with_under | | +| Classes | CapWords | _CapWords | +| Exceptions | CapWords | | +| Functions | lower_with_under() | _lower_with_under() | +| Global/Class Constants | CAPS_WITH_UNDER | _CAPS_WITH_UNDER | +| Global/Class Variables | lower_with_under | _lower_with_under | +| Instance Variables | lower_with_under | _lower_with_under (protected) or __lower_with_under (private) | +| Method Names | lower_with_under() | _lower_with_under() (protected) or __lower_with_under() (private) | +| Function/Method Parameters | lower_with_under | | +| Local Variables | lower_with_under | \ No newline at end of file diff --git a/sop-sdk/sdk-python/request/BaseRequest.py b/sop-sdk/sdk-python/request/BaseRequest.py new file mode 100644 index 00000000..9714150a --- /dev/null +++ b/sop-sdk/sdk-python/request/BaseRequest.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +from response.BaseResponse import BaseResponse + + +class BaseRequest: + """请求类的父类""" + + biz_model = None + """请求参数""" + + files = None + """上传文件""" + + def __init__(self): + pass + + def get_method(self): + """返回接口名 + + :return: 返回接口名 + :rtype: str + """ + raise Exception('未实现BaseRequest.get_method()方法') + + def get_version(self): + """返回接口版本号 + + :return: 返回版本号,如:1.0 + :rtype: str + """ + raise Exception('未实现BaseRequest.get_version()方法') + + def get_request_type(self): + """返回请求类型 + + :return: 返回RequestType类实例 + :rtype: common.RequestType + """ + raise Exception('未实现BaseRequest.get_request_type()方法') + + def parse_response(self, response_dict): + response_data = response_dict.get('error_response') + if response_data is None: + data_name = self.get_method().replace('.', '_') + '_response' + response_data = response_dict.get(data_name) + return response_data + + + + + + diff --git a/sop-sdk/sdk-python/request/MemberInfoGetRequest.py b/sop-sdk/sdk-python/request/MemberInfoGetRequest.py new file mode 100644 index 00000000..6df98e3e --- /dev/null +++ b/sop-sdk/sdk-python/request/MemberInfoGetRequest.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +from common import RequestTypes +from request.BaseRequest import BaseRequest + + +class MemberInfoGetRequest(BaseRequest): + """获取会员信息请求""" + + def __init__(self): + BaseRequest.__init__(self) + + def get_method(self): + return 'member.info.get' + + def get_version(self): + return '1.0' + + def get_request_type(self): + return RequestTypes.GET diff --git a/sop-sdk/sdk-python/request/__init__.py b/sop-sdk/sdk-python/request/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sop-sdk/sdk-python/response/BaseResponse.py b/sop-sdk/sdk-python/response/BaseResponse.py new file mode 100644 index 00000000..4df2c650 --- /dev/null +++ b/sop-sdk/sdk-python/response/BaseResponse.py @@ -0,0 +1,23 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +from UserDict import UserDict + + +class BaseResponse(UserDict): + """返回类""" + request_id = None + code = None + msg = None + sub_code = None + sub_msg = None + + def __init__(self, _dict=None, **kwargs): + UserDict.__init__(self, _dict, **kwargs) + + def is_success(self): + """是否成功 + + :return: True,成功 + :rtype: bool + """ + return self.sub_code is None diff --git a/sop-sdk/sdk-python/response/__init__.py b/sop-sdk/sdk-python/response/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sop-sdk/sdk-python/test/__init__.py b/sop-sdk/sdk-python/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sop-sdk/sdk-python/test/aa.txt b/sop-sdk/sdk-python/test/aa.txt new file mode 100644 index 00000000..524527cb --- /dev/null +++ b/sop-sdk/sdk-python/test/aa.txt @@ -0,0 +1 @@ +hello你好123 \ No newline at end of file diff --git a/sop-sdk/sdk-python/test/bb.txt b/sop-sdk/sdk-python/test/bb.txt new file mode 100644 index 00000000..85f8e329 --- /dev/null +++ b/sop-sdk/sdk-python/test/bb.txt @@ -0,0 +1 @@ +文件bb的内容 \ No newline at end of file diff --git a/sop-sdk/sdk-python/test/test.py b/sop-sdk/sdk-python/test/test.py new file mode 100644 index 00000000..f98400cf --- /dev/null +++ b/sop-sdk/sdk-python/test/test.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import unittest + +from common import JsonUtil +from model.MemberInfoGetModel import MemberInfoGetModel +from request.MemberInfoGetRequest import MemberInfoGetRequest +from common.OpenClient import OpenClient + +import sys + +reload(sys) +sys.setdefaultencoding('utf8') + + +class MyTestCase(unittest.TestCase): + # 应用id + app_id = '201904035630907729292csharp' + # 应用私钥 + private_key = 'MIIEowIBAAKCAQEA5+OvJxeSzf44NxQ/cl7Ii+BzPg2k6sRcvH4ffOtU5Dzq1/oEvg02nxIhmwOHBZmjbmuUu0aLsfglUTAwqfXftfAKZidshsgj9NNh0/kxk0avRZ1UoljWGz/FxVZA0ogbxxhohPZ9jWcD+eBQcIwF2DtHfAJqWWZrYFnCMeHD8mPzxo2kwXSvDzi0vf9I2tKiYvNG26a9FqeYtPOoi81sdS3+70HOMdxP8ejXtyfnKpKz7Dx506LCIRS5moWS3Q5eTLV3NGX/1CSJ8wpQA2DAQTjVhX5eVu7Yqz12t8W+sjWM/tHUR6cgwYYR10p7tSCeCPzkigjGxKm4cYXWtATQJQIDAQABAoIBAHFDsgrrJca+NKEan77ycwx3jnKx4WrWjOF4zVKL9AQjiSYDNgvKknJyPb3kpC/lEoHdxGERHSzJoxib7DkoIqRQYhPxj73pxj5QfYk3P7LLJNNg/LTrpXDb3nL8JV9wIflGf87qQvstZTDJEyFWE4jBs7Hr0BxovWvri8InnzkmERJ1cbGJgNHe1Y3Zo2tw0yaHxQCxLuajP+notRZhD9bEp7uKeI0w9AvlW6k8m/7y10F0BK/TlyW8rQiEC391yOiRYoMcUh4hd2Q9bMx3jngZgX8PXIvZZcup4/pvWlv1alwhB2tsnLdazP62r1MO80vLyLunzGO+7WwCjEYlVaECgYEA+lQRFmbhKaPuAuXMtY31Fbga8nedka5TjnEV7+/kX+yowE2OlNujF+ZG8UTddTxAGv56yVNi/mjRlgD74j8z0eOsgvOq9mwbCrgLhLo51H9O/wAxtb+hBKtC5l50pBr4gER6d8W6EQNTSGojnMIaLXTkAZ5Qf6Z8e2HFVdOn0X0CgYEA7SSrTokwzukt5KldNu5ukyyd+C3D1i6orbg6qD73EP9CfNMfGSBn7dDv9wMSJH01+Ty+RgTROgtjGRDbMJWnfbdt/61NePr9ar5sb6Nbsf7/I0w7cZF5dsaFYgzaOfQYquzXPbLQHkpMT64bqpv/Mwy4F2lFvaYWY5fA4pC2uckCgYEAg75Ym9ybJaoTqky8ttQ2Jy8UZ4VSVQhVC0My02sCWwWXLlXi8y7An+Rec73Ve0yxREOn5WrQT6pkmzh7V/ABWrYi5WxODpCIjtSbo0fLBa3Wqle00b0/hdCITetqIa/cFs1zUrOqICgK3bKWeXqiAkhhcwSZwwSgwOKM04Wn7ZUCgYBvhHX2mbdVJfyJ8kc+hMOE/E9RHRxiBVEXWHJlGi8PVCqNDq8qHr4g7Mdbzprig+s0yKblwHAvrpkseWvKHiZEjVTyDipHgShY4TGXEigVvUd37uppTrLi8xpYcJjS9gH/px7VCdiq1d+q/MJP6coJ1KphgATm2UrgDMYNBWaYWQKBgEHRxrmER7btUF60/YgcqPHFc8RpYQB2ZZE0kyKGDqk2Data1XYUY6vsPAU28yRLAaWr/D2H17iyLkxP80VLm6QhifxCadv90Q/Wl1DFfOJQMW6avyQ0so6G0wFq/LJxaFK4iLXQn1RJnmTp6BYiJMmK2BhFbRzw8ssMoF6ad2rr' + # 请求URL + url = 'http://localhost:8081' + # 创建请求客户端 + client = OpenClient(app_id, private_key, url) + + def test_api(self): + # 创建请求 + request = MemberInfoGetRequest() + # 请求参数 + model = MemberInfoGetModel() + model.age = 22 + model.name = 'jim' + model.address = 'xx' + # 添加请求参数 + request.biz_model = model + + # 添加上传文件 + # files = { + # 'file1': open('aa.txt', 'rb'), + # 'file2': open('bb.txt', 'rb') + # } + # request.files = files + + # 调用请求 + response = self.client.execute(request) + + # 关闭文件 + # for f in files.values(): + # f.close() + + if response.is_success(): + print 'response: ', response + print 'is_vip:', response.get('member_info').get('is_vip', 0) + else: + print '请求失败,code:%s, msg:%s, sub_code:%s, sub_msg:%s' % \ + (response.code, response.msg, response.sub_code, response.sub_msg) + + def test_to_json_string(self): + model = MemberInfoGetModel() + model.age = 1 + model.name = '张三' + model.address = 'xx' + json_string = JsonUtil.to_json_string(model) + print 'json:', json_string + + +if __name__ == '__main__': + unittest.main() diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/controller/SandboxController.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/controller/SandboxController.java index 7afb7bd2..7c04af0d 100644 --- a/sop-website/src/main/java/com/gitee/sop/websiteserver/controller/SandboxController.java +++ b/sop-website/src/main/java/com/gitee/sop/websiteserver/controller/SandboxController.java @@ -59,6 +59,7 @@ public class SandboxController { public SandboxResult proxy( @RequestParam String appId , @RequestParam String privateKey + , @RequestParam(required = false) String token , @RequestParam String method , @RequestParam String version , @RequestParam String bizContent @@ -82,6 +83,10 @@ public class SandboxController { params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); params.put("version", version); + if (StringUtils.isNotBlank(token)) { + params.put("app_auth_token", token); + } + // 业务参数 params.put("biz_content", bizContent); diff --git a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/SwaggerDocParser.java b/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/SwaggerDocParser.java index c7e366d2..f47e3c21 100644 --- a/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/SwaggerDocParser.java +++ b/sop-website/src/main/java/com/gitee/sop/websiteserver/manager/SwaggerDocParser.java @@ -208,8 +208,11 @@ public class SwaggerDocParser implements DocParser { protected List buildDocParameters(String ref, JSONObject docRoot, boolean doSubRef) { JSONObject responseObject = docRoot.getJSONObject("definitions").getJSONObject(ref); JSONObject properties = responseObject.getJSONObject("properties"); - Set fieldNames = properties.keySet(); List docParameterList = new ArrayList<>(); + if (properties == null) { + return docParameterList; + } + Set fieldNames = properties.keySet(); for (String fieldName : fieldNames) { /* { diff --git a/sop-website/src/main/resources/public/pages/sandbox/sandbox.html b/sop-website/src/main/resources/public/pages/sandbox/sandbox.html index 3514d938..0f389a68 100644 --- a/sop-website/src/main/resources/public/pages/sandbox/sandbox.html +++ b/sop-website/src/main/resources/public/pages/sandbox/sandbox.html @@ -117,6 +117,10 @@ PrivateKey + + Token + + diff --git a/sop-website/src/main/resources/public/pages/sandbox/sandbox.js b/sop-website/src/main/resources/public/pages/sandbox/sandbox.js index 851f0f56..4798ec18 100644 --- a/sop-website/src/main/resources/public/pages/sandbox/sandbox.js +++ b/sop-website/src/main/resources/public/pages/sandbox/sandbox.js @@ -43,7 +43,7 @@ function selectItem(docItem, layui) { function buildHttpMethodOptions(docItem) { var methodList = docItem.httpMethodList; - var html = [] + var html = []; for (var i = 0; i < methodList.length; i++) { var method = methodList[i]; html.push(''); @@ -118,6 +118,7 @@ function doTest() { var data = { appId: $('#appId').val() , privateKey: $('#privateKey').val() + , token: $('#token').val() , method: method , version: version , httpMethod: $('#httpMethodList').val()