This commit is contained in:
六如
2024-10-10 11:35:34 +08:00
parent d9fb25dc0a
commit e23911b075
193 changed files with 2435 additions and 1008 deletions

View File

@@ -1,25 +0,0 @@
/target/
!.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/

View File

@@ -1,74 +1,251 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-gateway</artifactId>
<version>5.0.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- springboot 版本-->
<spring-boot.version>2.6.15</spring-boot.version>
<!-- spring cloud 版本 -->
<spring-cloud.version>2021.0.5</spring-cloud.version>
<!-- spring cloud alibaba 版本 -->
<!-- 具体版本对应关系见https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<!-- dubbo版本 -->
<dubbo.version>3.2.10</dubbo.version>
<junit.version>4.11</junit.version>
<commons-io.version>2.5</commons-io.version>
<commons-fileupload.version>1.3.3</commons-fileupload.version>
<commons-collection.version>3.2.2</commons-collection.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<commons-codec.version>1.11</commons-codec.version>
<commons-logging.version>1.2</commons-logging.version>
<validation-api.version>2.0.1.Final</validation-api.version>
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
<fastmybatis.version>3.0.10</fastmybatis.version>
</properties>
<dependencies>
<!--
<artifactId>sop-bridge-nacos</artifactId> : 使用nacos注册中心
<artifactId>sop-bridge-eureka</artifactId> : 使用eureka注册中心
-->
<dependency>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-bridge-nacos</artifactId>
<!-- <artifactId>sop-bridge-eureka</artifactId>-->
<version>${project.version}</version>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- nacos注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!-- test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- provided -->
<!-- 仅在开发中使用 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- 加了这个就不需要加版本号了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
<version>${fastmybatis.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.7</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validation-api.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collection.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<build>
<plugins>
@@ -85,20 +262,17 @@
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>maven2</id>
<name>maven2</name>
<url>https://repo1.maven.org/maven2</url>
</repository>
</repositories>
</project>

View File

@@ -1,12 +0,0 @@
# sop-gateway
网关入口默认使用的是Spring Cloud Gateway如果要使用Zuul修改pom.xml
```xml
<dependency>
<groupId>com.gitee.sop</groupId>
<!--<artifactId>sop-bridge-gateway</artifactId>-->
<artifactId>sop-bridge-zuul</artifactId>
<version>version</version>
</dependency>
```

View File

@@ -1,18 +1,15 @@
package com.gitee.sop.gateway;
import org.mybatis.spring.annotation.MapperScan;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan(basePackages = {
"com.gitee.sop.gateway.mapper"
})
@SpringBootApplication(scanBasePackages = "com.gitee.sop")
@SpringBootApplication
@EnableDubbo
public class SopGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SopGatewayApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(SopGatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,70 @@
package com.gitee.sop.gateway.common;
import lombok.Data;
import java.io.Serializable;
/**
* @author 六如
*/
@Data
public class ApiInfoDTO implements Serializable {
private static final long serialVersionUID = 2183251167679411550L;
/**
* 应用名称
*/
private String application;
/**
* 接口名称
*/
private String apiName;
/**
* 版本号
*/
private String apiVersion;
/**
* 接口描述
*/
private String description;
/**
* 备注
*/
private String remark;
/**
* 接口class
*/
private String interfaceClassName;
/**
* 方法名称
*/
private String methodName;
/**
* 参数信息
*/
private String paramInfo;
/**
* 接口是否需要授权访问
*/
private Integer isPermission;
/**
* 是否需要appAuthToken
*/
private Integer isNeedToken;
private Integer status;
public String buildApiNameVersion() {
return apiName + apiVersion;
}
}

View File

@@ -0,0 +1,21 @@
package com.gitee.sop.gateway.common;
/**
* @author 六如
*/
public class AttachmentNames {
/** 分配给开发者的应用ID */
public static final String APP_ID_NAME = "client.app_id";
/** 接口名称 */
public static final String API_NAME = "client.method";
/** 调用的接口版本 */
public static final String VERSION_NAME = "client.version";
/** 开放平台主动通知商户服务器里指定的页面http/https路径 */
public static final String NOTIFY_URL_NAME = "client.notify_url";
/** OAuth 2.0授权token */
public static final String APP_AUTH_TOKEN_NAME = "client.app_auth_token";
public static final String CLIENT_IP = "client.ip";
public static final String TRACE_ID = "client.trace_id";
}

View File

@@ -0,0 +1,10 @@
package com.gitee.sop.gateway.common;
import lombok.Data;
@Data
public class ParamInfoDTO {
private String name;
private String type;
private String actualType;
}

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.gateway.common;
import com.gitee.sop.gateway.request.ApiRequestContext;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 六如
*/
@Getter
@AllArgsConstructor
public class RouteContext {
private ApiRequestContext apiRequestContext;
private ApiInfoDTO apiInfo;
}

View File

@@ -0,0 +1,16 @@
package com.gitee.sop.gateway.common;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author 六如
*/
public class SopConstants {
private SopConstants() {}
public static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
public static final String UTF8 = "UTF-8";
}

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.gateway.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 六如
*/
@AllArgsConstructor
@Getter
public enum StatusEnum {
ENABLE(1),
DISABLE(2);
private final int value;
}

View File

@@ -0,0 +1,89 @@
package com.gitee.sop.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author 六如
*/
@Configuration
@ConfigurationProperties(prefix = "api")
@Data
public class ApiConfig {
// ========= 请求参数名 =========
/**
* 分配给开发者的应用ID
*/
private String appIdName = "app_id";
/**
* 接口名称
*/
private String apiName = "method";
/**
* 仅支持JSON
*/
private String formatName = "format";
/**
* 请求使用的编码格式
*/
private String charsetName = "charset";
/**
* 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2
*/
private String signTypeName = "sign_type";
/**
* 商户请求参数的签名串
*/
private String signName = "sign";
/**
* 发送请求的时间
*/
private String timestampName = "timestamp";
/**
* 调用的接口版本
*/
private String versionName = "version";
/**
* 开放平台主动通知商户服务器里指定的页面http/https路径
*/
private String notifyUrlName = "notify_url";
/**
* OAuth 2.0授权token
*/
private String appAuthTokenName = "app_auth_token";
/**
* 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
*/
private String bizContentName = "biz_content";
// ========= 请求参数名 end =========
/**
* 超时时间
*/
private int timeoutSeconds = 60 * 5;
/**
* 是否开启限流功能
*/
private boolean openLimit = true;
/**
* 显示返回sign
*/
private boolean showReturnSign = true;
/**
* 时间戳格式
*/
private String timestampPattern = "yyyy-MM-dd HH:mm:ss";
/**
* 时区,默认:Asia/Shanghai
*/
private String zoneId = "Asia/Shanghai";
}

View File

@@ -0,0 +1,14 @@
package com.gitee.sop.gateway.config;
import org.springframework.context.annotation.Configuration;
/**
* 自定的扩展组件放这里
*
* @author 六如
*/
@Configuration
public class CustomConfig {
}

View File

@@ -0,0 +1,93 @@
package com.gitee.sop.gateway.config;
import com.gitee.sop.gateway.message.ErrorFactory;
import com.gitee.sop.gateway.service.ParamExecutor;
import com.gitee.sop.gateway.service.ParamExecutorImpl;
import com.gitee.sop.gateway.service.RouteService;
import com.gitee.sop.gateway.service.RouteServiceImpl;
import com.gitee.sop.gateway.service.interceptor.internal.ResultRouteInterceptor;
import com.gitee.sop.gateway.service.manager.ApiManager;
import com.gitee.sop.gateway.service.manager.IsvManager;
import com.gitee.sop.gateway.service.manager.SecretManager;
import com.gitee.sop.gateway.service.manager.impl.LocalApiManagerImpl;
import com.gitee.sop.gateway.service.manager.impl.LocalIsvManagerImpl;
import com.gitee.sop.gateway.service.manager.impl.LocalSecretManagerImpl;
import com.gitee.sop.gateway.service.manager.impl.RedisApiManagerImpl;
import com.gitee.sop.gateway.service.manager.impl.RedisIsvManagerImpl;
import com.gitee.sop.gateway.service.manager.impl.RedisSecretManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author 六如
*/
@Configuration
@Slf4j
public class IndexConfig {
@Bean
@ConditionalOnProperty(value = "gateway.manager.api", havingValue = "local", matchIfMissing = true)
public ApiManager localApiManager() {
return new LocalApiManagerImpl();
}
@Bean
@ConditionalOnProperty(value = "gateway.manager.api", havingValue = "redis")
public ApiManager redisApiManager() {
return new RedisApiManagerImpl();
}
@Bean
@ConditionalOnProperty(value = "gateway.manager.isv", havingValue = "local", matchIfMissing = true)
public IsvManager localIsvManager() {
return new LocalIsvManagerImpl();
}
@Bean
@ConditionalOnProperty(value = "gateway.manager.isv", havingValue = "redis")
public IsvManager redisIsvManager() {
return new RedisIsvManagerImpl();
}
@Bean
@ConditionalOnProperty(value = "gateway.manager.secret", havingValue = "local", matchIfMissing = true)
public SecretManager localSecretManager() {
return new LocalSecretManagerImpl();
}
@Bean
@ConditionalOnProperty(value = "gateway.manager.secret", havingValue = "redis")
public SecretManager redisSecretManager() {
return new RedisSecretManager();
}
// DEFAULT ROUTE INTERCEPTOR
@Bean
@ConditionalOnMissingBean
public ResultRouteInterceptor resultRouteInterceptor() {
return new ResultRouteInterceptor();
}
@Bean
@ConditionalOnMissingBean
public ParamExecutor paramExecutor() {
return new ParamExecutorImpl();
}
@Bean
@ConditionalOnMissingBean
public RouteService routeService() {
return new RouteServiceImpl();
}
@PostConstruct
public void init() {
ErrorFactory.initMessageSource(null);
}
}

View File

@@ -1,23 +0,0 @@
package com.gitee.sop.gateway.config;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class MyConfig {
@PostConstruct
public void after() {
ApiConfig.getInstance().setTokenValidator(apiParam -> {
// 获取客户端传递过来的token
String token = apiParam.fetchAccessToken();
return !StringUtils.isBlank(token);
// TODO: 校验token有效性可以从redis中读取
// 返回true表示这个token真实、有效
});
}
}

View File

@@ -0,0 +1,63 @@
package com.gitee.sop.gateway.controller;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.service.ParamExecutor;
import com.gitee.sop.gateway.service.RouteService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 开放平台入口
*
* @author 六如
*/
@Controller
public class IndexController {
@Resource
private RouteService routeService;
@Resource
private ParamExecutor<HttpServletRequest, HttpServletResponse> paramExecutor;
@GetMapping("/")
public void home(HttpServletResponse response) throws IOException {
response.getWriter().write("Open Platform");
// 跳转到网站首页
// response.sendRedirect("https://www.baidu.com");
}
/**
* 请求入口
*
* @apiNote 参数描述
<pre>
参数 类型 是否必填 最大长度 描述 示例值
app_id String 是 32 支付宝分配给开发者的应用ID 2014072300007148
method String 是 128 接口名称 alipay.trade.fastpay.refund.query
format String 否 40 仅支持JSON JSON
charset String 是 10 请求使用的编码格式如utf-8,gbk,gb2312等 utf-8
sign_type String 是 10 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2 RSA2
sign String 是 344 商户请求参数的签名串,详见签名 详见示例
timestamp String 是 19 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" 2014-07-24 03:07:50
version String 是 3 调用的接口版本固定为1.0 1.0
app_auth_token String 否 40 详见应用授权概述
biz_content String 是 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
</pre>
*/
@RequestMapping(value = "${gateway.path:/api}", method = {RequestMethod.GET, RequestMethod.POST})
public void index(HttpServletRequest request, HttpServletResponse response) throws IOException {
ApiRequestContext apiRequestContext = paramExecutor.build(request);
ApiResponse apiResponse = routeService.route(apiRequestContext);
paramExecutor.write(apiRequestContext, apiResponse, response);
}
}

View File

@@ -0,0 +1,89 @@
package com.gitee.sop.gateway.dao.entity;
import java.time.LocalDateTime;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
/**
* 表名api_info
* 备注:接口信息表
*
* @author 六如
*/
@Table(name = "api_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class ApiInfo {
private Long id;
/**
* 应用名称
*/
private String application;
/**
* 接口名称
*/
private String apiName;
/**
* 版本号
*/
private String apiVersion;
/**
* 接口描述
*/
private String description;
/**
* 备注
*/
private String remark;
/**
* 接口class
*/
private String interfaceClassName;
/**
* 方法名称
*/
private String methodName;
/**
* 参数信息
*/
private String paramInfo;
/**
* 接口是否需要授权访问
*/
private Integer isPermission;
/**
* 是否需要appAuthToken
*/
private Integer isNeedToken;
/**
* 注册来源1-系统注册,2-手动注册
*/
private Integer regSource;
/**
* 1启用2禁用
*/
private Integer status;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,45 @@
package com.gitee.sop.gateway.dao.entity;
import java.time.LocalDateTime;
import java.util.Date;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
/**
* 表名isv_info
* 备注isv信息表
*
* @author 六如
*/
@Table(name = "isv_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class IsvInfo {
private Long id;
/**
* appKey
*/
private String appId;
/**
* 1启用2禁用
*/
private Integer status;
/**
* 备注
*/
private String remark;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,57 @@
package com.gitee.sop.gateway.dao.entity;
import java.time.LocalDateTime;
import java.util.Date;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
/**
* 表名isv_keys
* 备注ISV秘钥管理
*
* @author 六如
*/
@Table(name = "isv_keys", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class IsvKeys {
private Long id;
private String appId;
/**
* 秘钥格式1PKCS8(JAVA适用)2PKCS1(非JAVA适用)
*/
private Integer keyFormat;
/**
* 开发者生成的公钥
*/
private String publicKeyIsv;
/**
* 开发者生成的私钥(交给开发者)
*/
private String privateKeyIsv;
/**
* 平台生成的公钥(交给开发者)
*/
private String publicKeyPlatform;
/**
* 平台生成的私钥
*/
private String privateKeyPlatform;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,19 @@
package com.gitee.sop.gateway.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.gateway.dao.entity.ApiInfo;
/**
* @author 六如
*/
public interface ApiInfoMapper extends BaseMapper<ApiInfo> {
default ApiInfo getByNameVersion(String apiName, String apiVersion) {
return this.query()
.eq(ApiInfo::getApiName, apiName)
.eq(ApiInfo::getApiVersion, apiVersion)
.get();
}
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.gateway.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.gateway.dao.entity.IsvInfo;
/**
* @author 六如
*/
public interface IsvInfoMapper extends BaseMapper<IsvInfo> {
default IsvInfo getByAppId(String appId) {
return this.get(IsvInfo::getAppId, appId);
}
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.gateway.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.gateway.dao.entity.IsvKeys;
/**
* @author 六如
*/
public interface IsvKeysMapper extends BaseMapper<IsvKeys> {
default IsvKeys getByAppId(String appId) {
return this.get(IsvKeys::getAppId, appId);
}
}

View File

@@ -1,36 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名config_gray
* 备注:服务灰度配置
*
* @author 六如
*/
@Table(name = "config_gray",pk = @Pk(name = "id"))
@Data
public class ConfigGray {
/** 数据库字段id */
private Long id;
/** 数据库字段service_id */
private String serviceId;
/** 用户key多个用引文逗号隔开, 数据库字段user_key_content */
private String userKeyContent;
/** 需要灰度的接口goods.get1.0=1.2,多个用英文逗号隔开, 数据库字段name_version_content */
private String nameVersionContent;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,35 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名config_gray_instance
*
* @author 六如
*/
@Table(name = "config_gray_instance",pk = @Pk(name = "id"))
@Data
public class ConfigGrayInstance {
/** 数据库字段id */
private Long id;
/** instance_id, 数据库字段instance_id */
private String instanceId;
/** service_id, 数据库字段service_id */
private String serviceId;
/** 0禁用1启用, 数据库字段status */
private Byte status;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,61 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名config_limit
* 备注:限流配置
*
* @author 六如
*/
@Table(name = "config_limit",pk = @Pk(name = "id"))
@Data
public class ConfigLimit {
/** 数据库字段id */
private Long id;
/** 路由id, 数据库字段route_id */
private String routeId;
/** 数据库字段app_key */
private String appKey;
/** 限流ip多个用英文逗号隔开, 数据库字段limit_ip */
private String limitIp;
/** 服务id, 数据库字段service_id */
private String serviceId;
/** 限流策略1窗口策略2令牌桶策略, 数据库字段limit_type */
private Byte limitType;
/** 每秒可处理请求数, 数据库字段exec_count_per_second */
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段limit_code */
private String limitCode;
/** 返回的错误信息, 数据库字段limit_msg */
private String limitMsg;
/** 令牌桶容量, 数据库字段token_bucket_count */
private Integer tokenBucketCount;
/** 限流开启状态1:开启0关闭, 数据库字段limit_status */
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段order_index */
private Integer orderIndex;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,26 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
/**
* 表名config_route_base
* 备注:路由配置表
*
* @author 六如
*/
@Table(name = "config_route_base",pk = @Pk(name = "id"))
@Data
public class ConfigRouteBase {
/** 数据库字段id */
private Long id;
/** 路由id, 数据库字段route_id */
private String routeId;
/** 状态1启用2禁用, 数据库字段status */
private Byte status;
}

View File

@@ -1,66 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名config_service_route
* 备注:路由配置
*
* @author 六如
*/
@Table(name = "config_service_route",pk = @Pk(name = "id"))
@Data
public class ConfigServiceRoute {
/** 数据库字段id */
private String id;
/** 数据库字段service_id */
private String serviceId;
/** 接口名, 数据库字段name */
private String name;
/** 版本号, 数据库字段version */
private String version;
/** 路由断言SpringCloudGateway专用, 数据库字段predicates */
private String predicates;
/** 路由过滤器SpringCloudGateway专用, 数据库字段filters */
private String filters;
/** 路由规则转发的目标uri, 数据库字段uri */
private String uri;
/** uri后面跟的path, 数据库字段path */
private String path;
/** 路由执行的顺序, 数据库字段order_index */
private Integer orderIndex;
/** 是否忽略验证,业务参数验证除外, 数据库字段ignore_validate */
private Byte ignoreValidate;
/** 状态0待审核1启用2禁用, 数据库字段status */
private Byte status;
/** 是否合并结果, 数据库字段merge_result */
private Byte mergeResult;
/** 是否需要授权才能访问, 数据库字段permission */
private Byte permission;
/** 是否需要token, 数据库字段need_token */
private Byte needToken;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,28 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class IsvDetailDTO {
/** appKey, 数据库字段app_key */
private String appKey;
/** 0启用1禁用, 数据库字段status */
private Byte status;
/** secret, 数据库字段secret */
private String secret;
/** 开发者生成的公钥, 数据库字段public_key_isv */
private String publicKeyIsv;
/** 平台生成的私钥, 数据库字段private_key_platform */
private String privateKeyPlatform;
/** 签名类型1:RSA2,2:MD5 */
private Byte signType;
}

View File

@@ -1,36 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名isv_info
* 备注isv信息表
*
* @author 六如
*/
@Table(name = "isv_info",pk = @Pk(name = "id"))
@Data
public class IsvInfo {
/** 数据库字段id */
private Long id;
/** appKey, 数据库字段app_key */
private String appKey;
/** 1启用2禁用, 数据库字段status */
private Byte status;
/** 1:RSA2,2:MD5, 数据库字段sign_type */
private Byte signType;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,61 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 表名monitor_info
* 备注:接口监控信息
*
* @author 六如
*/
@Table(name = "monitor_info",pk = @Pk(name = "id"))
@Data
public class MonitorInfo {
/** 数据库字段id */
private Long id;
/** 路由id, 数据库字段route_id */
private String routeId;
/** 接口名, 数据库字段name */
private String name;
/** 版本号, 数据库字段version */
private String version;
/** 数据库字段service_id */
private String serviceId;
/** 数据库字段instance_id */
private String instanceId;
/** 请求耗时最长时间, 数据库字段max_time */
private Integer maxTime;
/** 请求耗时最小时间, 数据库字段min_time */
private Integer minTime;
/** 总时长,毫秒, 数据库字段total_time */
private Long totalTime;
/** 总调用次数, 数据库字段total_request_count */
private Long totalRequestCount;
/** 成功次数, 数据库字段success_count */
private Long successCount;
/** 失败次数(业务主动抛出的异常算作成功,如参数校验,未知的错误算失败), 数据库字段error_count */
private Long errorCount;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,49 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 表名monitor_info_error
*
* @author 六如
*/
@Table(name = "monitor_info_error",pk = @Pk(name = "id"))
@Data
public class MonitorInfoError {
/** 数据库字段id */
private Long id;
/** 错误id,md5Hex(instanceId + routeId + errorMsg), 数据库字段error_id */
private String errorId;
/** 实例id, 数据库字段instance_id */
private String instanceId;
/** 数据库字段route_id */
private String routeId;
/** 数据库字段error_msg */
private String errorMsg;
/** http status非200错误, 数据库字段error_status */
private Integer errorStatus;
/** 错误次数, 数据库字段count */
private Integer count;
/** 数据库字段is_deleted */
@com.gitee.fastmybatis.core.annotation.LogicDelete
private Byte isDeleted;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,33 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名perm_isv_role
* 备注isv角色
*
* @author 六如
*/
@Table(name = "perm_isv_role",pk = @Pk(name = "id"))
@Data
public class PermIsvRole {
/** 数据库字段id */
private Long id;
/** isv_info表id, 数据库字段isv_id */
private Long isvId;
/** 角色code, 数据库字段role_code */
private String roleCode;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -1,33 +0,0 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.Table;
import java.util.Date;
/**
* 表名perm_role_permission
* 备注:角色权限表
*
* @author 六如
*/
@Table(name = "perm_role_permission",pk = @Pk(name = "id"))
@Data
public class PermRolePermission {
/** 数据库字段id */
private Long id;
/** 角色表code, 数据库字段role_code */
private String roleCode;
/** api_id, 数据库字段route_id */
private String routeId;
/** 数据库字段gmt_create */
private Date gmtCreate;
/** 数据库字段gmt_modified */
private Date gmtModified;
}

View File

@@ -0,0 +1,47 @@
package com.gitee.sop.gateway.exception;
import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.message.IError;
import java.util.Locale;
/**
* @author 六如
*/
public class ApiException extends RuntimeException {
private static final long serialVersionUID = 8278005515613227643L;
private final transient Locale locale;
private final transient ErrorEnum errorEnum;
private final transient Object[] params;
public ApiException(ErrorEnum errorEnum, Locale locale, Object... params) {
this.errorEnum = errorEnum;
this.params = params;
this.locale = locale;
}
public ApiException(Throwable cause, ErrorEnum errorEnum, Locale locale, Object... params) {
super(cause);
this.errorEnum = errorEnum;
this.params = params;
this.locale = locale;
}
public IError getError() {
return errorEnum.getErrorMeta().getError(locale, params);
}
public ErrorEnum getErrorEnum() {
return errorEnum;
}
@Override
public String getMessage() {
String message = super.getMessage();
return message == null ? errorEnum.getErrorMeta().toString() : message;
}
}

View File

@@ -0,0 +1,77 @@
package com.gitee.sop.gateway.exception;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.message.ErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class ControllerGlobalExceptionHandler {
// 未知异常
@ExceptionHandler(value = Exception.class)
public ApiResponse globalExceptionHandler(Exception e, HttpServletRequest request) {
log.error("系统出错", e);
return ApiResponse.error(ErrorEnum.ISP_UNKNOWN_ERROR, request.getLocale());
}
/**
* 处理jsr303的字段校验异常也可以自定义注解校验
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse handleValidationExceptions(
MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ApiResponse.error(ErrorEnum.ISV_ERROR_PARAMETER, request.getLocale(), String.join(",", errors.values()));
}
/**
* 验证异常
*
* @param e e
*/
@ExceptionHandler(ValidationException.class)
public ApiResponse validationException(ValidationException e, HttpServletRequest request) {
List<String> msgList = new ArrayList<>();
for (ConstraintViolation<?> constraintViolation : ((ConstraintViolationException) e).getConstraintViolations()) {
msgList.add(constraintViolation.getMessage());
}
return ApiResponse.error(ErrorEnum.ISV_ERROR_PARAMETER, request.getLocale(), String.join(",", msgList));
}
/**
* 参数绑定异常GET请求参数校验
*
* @param e e
*/
@ExceptionHandler(BindException.class)
public ApiResponse bandException(BindException e, HttpServletRequest request) {
List<FieldError> fieldErrors = e.getFieldErrors();
List<String> msgList = new ArrayList<>();
for (FieldError fieldError : fieldErrors) {
msgList.add(fieldError.getDefaultMessage());
}
return ApiResponse.error(ErrorEnum.ISV_ERROR_PARAMETER, request.getLocale(), String.join(",", msgList));
}
}

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.gateway.exception;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
/**
* @author 六如
*/
public interface ExceptionExecutor {
ApiResponse executeException(ApiRequestContext apiRequestContext, Exception e);
}

View File

@@ -0,0 +1,25 @@
package com.gitee.sop.gateway.exception;
import com.gitee.sop.gateway.message.ErrorEnum;
import lombok.Getter;
/**
* @author 六如
*/
@Getter
public class SignException extends Exception {
private static final long serialVersionUID = 4049365045207852768L;
public SignException(ErrorEnum errorEnum) {
this.errorEnum = errorEnum;
}
public SignException(ErrorEnum errorEnum, Throwable cause) {
super(cause);
this.errorEnum = errorEnum;
}
private final ErrorEnum errorEnum;
}

View File

@@ -0,0 +1,50 @@
package com.gitee.sop.gateway.exception.impl;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.exception.ExceptionExecutor;
import com.gitee.sop.gateway.message.ErrorEnum;
import org.apache.dubbo.rpc.service.GenericException;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author 六如
*/
@Service
public class ExceptionExecutorImpl implements ExceptionExecutor {
private static final String CONSTRAINT_VIOLATION_EXCEPTION = "ConstraintViolationException";
@Override
public ApiResponse executeException(ApiRequestContext apiRequestContext, Exception e) {
if (e instanceof GenericException) {
GenericException genericException = (GenericException) e;
String exceptionClass = genericException.getExceptionClass();
if (exceptionClass.contains(CONSTRAINT_VIOLATION_EXCEPTION)) {
String exceptionMessage = genericException.getExceptionMessage();
// 参数校验:Failed to validate service: com.gitee.sop.storyweb.open.StoryService, method: save, cause: [ConstraintViolationImpl{interpolatedMessage='故事名称必填', propertyPath=storyName, rootBeanClass=class com.gitee.sop.storyweb.open.req.StorySaveDTO, messageTemplate='故事名称必填'}]
Set<String> msgs = findErrorMsg(exceptionMessage);
return ApiResponse.error(ErrorEnum.ISV_ERROR_PARAMETER, apiRequestContext.getLocale(), String.join(",", msgs));
}
} else if (e instanceof ApiException) {
return ApiResponse.error(((ApiException) e));
}
return ApiResponse.error(ErrorEnum.ISP_SERVICE_UNKNOWN_ERROR, apiRequestContext.getLocale());
}
private static Set<String> findErrorMsg(String text) {
Pattern pattern = Pattern.compile("'([^']*)'");
Matcher matcher = pattern.matcher(text);
Set<String> msgList = new LinkedHashSet<>();
while (matcher.find()) {
msgList.add(matcher.group(1));
}
return msgList;
}
}

View File

@@ -1,43 +0,0 @@
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 com.gitee.sop.gatewaycommon.sync.SopAsyncConfigurer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 用于收集监控数据
*
* @author 六如
*/
@Component
@Slf4j
public class MonitorRouteInterceptor implements RouteInterceptor {
@Autowired
SopAsyncConfigurer sopAsyncConfigurer;
@Autowired
MonitorRouteInterceptorService monitorRouteInterceptorService;
@Override
public void preRoute(RouteInterceptorContext context) {
}
@Override
public void afterRoute(RouteInterceptorContext context) {
sopAsyncConfigurer.getAsyncExecutor().execute(()-> {
monitorRouteInterceptorService.storeRequestInfo(context);
});
}
@Override
public int getOrder() {
return -1000;
}
}

View File

@@ -1,164 +0,0 @@
package com.gitee.sop.gateway.interceptor;
import com.gitee.sop.gateway.manager.DbMonitorInfoManager;
import com.gitee.sop.gatewaycommon.bean.LRUCache;
import com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext;
import com.gitee.sop.gatewaycommon.monitor.MonitorDTO;
import com.gitee.sop.gatewaycommon.monitor.MonitorData;
import com.gitee.sop.gatewaycommon.monitor.MonitorManager;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.sync.MyNamedThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author 六如
*/
@Service
@Slf4j
public class MonitorRouteInterceptorService {
/**
* 刷新到数据库时间间隔,秒
*/
@Value("${sop.monitor.flush-period-seconds:30}")
int flushPeriodSeconds;
/**
* 定时任务每n秒执行一次
*/
@Value("${sop.monitor.schedule-period-seconds:30}")
int schedulePeriodSeconds;
/**
* 错误数量容量
*/
@Value("${sop.monitor.error-count-capacity:50}")
int monitorErrorCapacity;
@Autowired
DbMonitorInfoManager dbMonitorInfoManager;
@Autowired
MonitorManager monitorManager;
/**
* 记录接口调用流量,最大时间,最小时间,总时长,平均时长,调用次数,成功次数,失败次数.
* 需要考虑并发情况。
*/
public synchronized void storeRequestInfo(RouteInterceptorContext context) {
ApiParam apiParam = context.getApiParam();
ServiceInstance serviceInstance = context.getServiceInstance();
String routeId = apiParam.getRouteId();
int spendTime = (int)(context.getFinishTimeMillis() - context.getBeginTimeMillis());
// 这步操作是线程安全的底层调用了ConcurrentHashMap.computeIfAbsent
String key = getMonitorKey(routeId, serviceInstance.getInstanceId());
MonitorData monitorData = monitorManager.getMonitorInfo(key, (k) -> this.createMonitorInfo(apiParam, serviceInstance));
monitorData.storeMaxTime(spendTime);
monitorData.storeMinTime(spendTime);
monitorData.getTotalRequestCount().incrementAndGet();
monitorData.getTotalTime().addAndGet(spendTime);
if (context.isSuccessRequest()) {
monitorData.getSuccessCount().incrementAndGet();
} else {
monitorData.getErrorCount().incrementAndGet();
String errorMsg = context.getServiceErrorMsg();
monitorData.addErrorMsg(errorMsg, context.getResponseStatus());
}
}
private String getMonitorKey(String routeId, String instanceId) {
return routeId + instanceId;
}
/**
* 刷新到数据库
*/
private synchronized void flushDb() {
Map<String, MonitorData> monitorData = monitorManager.getMonitorData();
if (monitorData.isEmpty()) {
return;
}
LocalDateTime checkTime = LocalDateTime.now();
List<String> tobeRemoveKeys = new ArrayList<>();
List<MonitorDTO> tobeSaveBatch = new ArrayList<>(monitorData.size());
monitorData.forEach((key, value) -> {
LocalDateTime flushTime = value.getFlushTime();
if (flushTime.isEqual(checkTime) || flushTime.isBefore(checkTime)) {
log.debug("刷新监控数据到数据库, MonitorData:{}", value);
tobeRemoveKeys.add(key);
MonitorDTO monitorDTO = getMonitorDTO(value);
tobeSaveBatch.add(monitorDTO);
}
});
dbMonitorInfoManager.saveMonitorInfoBatch(tobeSaveBatch);
for (String key : tobeRemoveKeys) {
monitorData.remove(key);
}
}
private MonitorDTO getMonitorDTO(MonitorData monitorData) {
MonitorDTO monitorDTO = new MonitorDTO();
monitorDTO.setRouteId(monitorData.getRouteId());
monitorDTO.setName(monitorData.getName());
monitorDTO.setVersion(monitorData.getVersion());
monitorDTO.setServiceId(monitorData.getServiceId());
monitorDTO.setInstanceId(monitorData.getInstanceId());
monitorDTO.setMaxTime(monitorData.getMaxTime());
monitorDTO.setMinTime(monitorData.getMinTime());
monitorDTO.setTotalTime(monitorData.getTotalTime().longValue());
monitorDTO.setTotalRequestCount(monitorData.getTotalRequestCount().longValue());
monitorDTO.setSuccessCount(monitorData.getSuccessCount().longValue());
monitorDTO.setErrorCount(monitorData.getErrorCount().longValue());
monitorDTO.setErrorMsgList(monitorData.getMonitorErrorMsgMap().values());
return monitorDTO;
}
private MonitorData createMonitorInfo(ApiParam apiParam, ServiceInstance serviceInstance) {
MonitorData monitorData = new MonitorData();
monitorData.setRouteId(apiParam.getRouteId());
monitorData.setName(apiParam.fetchName());
monitorData.setVersion(apiParam.fetchVersion());
monitorData.setServiceId(apiParam.fetchServiceId());
monitorData.setInstanceId(serviceInstance.getInstanceId());
monitorData.setTotalTime(new AtomicInteger());
monitorData.setMaxTime(0);
monitorData.setMinTime(0);
monitorData.setSuccessCount(new AtomicInteger());
monitorData.setTotalRequestCount(new AtomicInteger());
monitorData.setErrorCount(new AtomicInteger());
monitorData.setFlushTime(getFlushTime());
monitorData.setMonitorErrorMsgMap(new LRUCache<>(monitorErrorCapacity));
return monitorData;
}
private LocalDateTime getFlushTime() {
return LocalDateTime.now()
.plusSeconds(flushPeriodSeconds);
}
@PostConstruct
public void after() {
// 每隔schedulePeriodSeconds秒执行一次
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new MyNamedThreadFactory("monitorSchedule"));
// 延迟执行随机5~14秒
int delay = 5 + new Random().nextInt(10);
scheduledThreadPoolExecutor.scheduleWithFixedDelay(this::flushDb, delay, schedulePeriodSeconds, TimeUnit.SECONDS);
}
}

View File

@@ -1,49 +0,0 @@
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.net.URI;
/**
* 演示拦截器
*
* @author 六如
*/
@Slf4j
@Component
public class MyRouteInterceptor implements RouteInterceptor {
@Override
public void preRoute(RouteInterceptorContext context) {
ApiParam apiParam = context.getApiParam();
ServerWebExchange exchange = (ServerWebExchange) context.getRequestContext();
URI uri = exchange.getRequest().getURI();
log.info("请求URL:{}, 请求接口:{}, request_id:{}, app_id:{}, ip:{}",
uri,
apiParam.fetchNameVersion(),
apiParam.fetchRequestId(),
apiParam.fetchAppKey(),
apiParam.fetchIp()
);
}
@Override
public void afterRoute(RouteInterceptorContext context) {
String serviceErrorMsg = context.getServiceErrorMsg();
if (StringUtils.hasText(serviceErrorMsg)) {
log.error("错误信息:{}", serviceErrorMsg);
}
}
@Override
public int getOrder() {
return 0;
}
}

View File

@@ -1,120 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.entity.ConfigGray;
import com.gitee.sop.gateway.entity.ConfigGrayInstance;
import com.gitee.sop.gateway.mapper.ConfigGrayInstanceMapper;
import com.gitee.sop.gateway.mapper.ConfigGrayMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.bean.ServiceGrayDefinition;
import com.gitee.sop.gatewaycommon.manager.DefaultEnvGrayManager;
import com.gitee.sop.gatewaycommon.route.RegistryEvent;
import com.gitee.sop.gatewaycommon.loadbalancer.ServiceGrayConfig;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 存放用户key这里放在本机内容如果灰度发布保存的用户id数量偏多可放在redis中
*
* @author 六如
*/
@Slf4j
@Service
public class DbEnvGrayManager extends DefaultEnvGrayManager implements RegistryEvent {
private static final int STATUS_ENABLE = 1;
private static final Function<String[], String> FUNCTION_KEY = arr -> arr[0];
private static final Function<String[], String> FUNCTION_VALUE = arr -> arr[1];
@Autowired
private ConfigGrayMapper configGrayMapper;
@Autowired
private ConfigGrayInstanceMapper configGrayInstanceMapper;
@Override
public void onRegistry(InstanceDefinition instanceDefinition) {
String instanceId = instanceDefinition.getInstanceId();
ConfigGrayInstance grayInstance = configGrayInstanceMapper.getByColumn("instance_id", instanceId);
if (grayInstance != null && grayInstance.getStatus() == STATUS_ENABLE) {
log.info("实例[{}]开启灰度发布", grayInstance.getInstanceId());
this.openGray(grayInstance.getInstanceId(), grayInstance.getServiceId());
}
}
@Override
public void onRemove(String serviceId) {
}
@Override
public void load() {
List<ConfigGray> list = configGrayMapper.list(new Query());
for (ConfigGray configGray : list) {
this.setServiceGrayConfig(configGray);
}
}
/**
* 设置用户key
*
* @param configGray 灰度配置
*/
public void setServiceGrayConfig(ConfigGray configGray) {
if (configGray == null) {
return;
}
String userKeyData = configGray.getUserKeyContent();
String nameVersionContent = configGray.getNameVersionContent();
String[] userKeys = StringUtils.split(userKeyData, ',');
String[] nameVersionList = StringUtils.split(nameVersionContent, ',');
log.info("灰度配置userKeys.length:{}, nameVersionList:{}", userKeys.length, Arrays.toString(nameVersionList));
Set<String> userKeySet = Stream.of(userKeys)
.collect(Collectors.toCollection(Sets::newConcurrentHashSet));
Map<String, String> grayNameVersionMap = Stream.of(nameVersionList)
.map(nameVersion -> StringUtils.split(nameVersion, '='))
.collect(Collectors.toConcurrentMap(FUNCTION_KEY, FUNCTION_VALUE));
ServiceGrayConfig serviceGrayConfig = new ServiceGrayConfig();
serviceGrayConfig.setServiceId(configGray.getServiceId());
serviceGrayConfig.setUserKeys(userKeySet);
serviceGrayConfig.setGrayNameVersion(grayNameVersionMap);
this.saveServiceGrayConfig(serviceGrayConfig);
}
@Override
public void process(ChannelMsg channelMsg) {
ServiceGrayDefinition userKeyDefinition = channelMsg.toObject(ServiceGrayDefinition.class);
String serviceId = userKeyDefinition.getServiceId();
switch (channelMsg.getOperation()) {
case "set":
ConfigGray configGray = configGrayMapper.getByColumn("service_id", serviceId);
setServiceGrayConfig(configGray);
break;
case "open":
openGray(userKeyDefinition.getInstanceId(), serviceId);
break;
case "close":
closeGray(userKeyDefinition.getInstanceId());
break;
default:
}
}
}

View File

@@ -1,55 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.sop.gateway.mapper.IPBlacklistMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.manager.DefaultIPBlacklistManager;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 限流配置管理
*
* @author 六如
*/
@Slf4j
@Service
public class DbIPBlacklistManager extends DefaultIPBlacklistManager {
@Autowired
private IPBlacklistMapper ipBlacklistMapper;
@Override
public void load() {
List<String> ipList = ipBlacklistMapper.listAllIP();
log.info("加载IP黑名单, size:{}", ipList.size());
ipList.forEach(this::add);
}
@Override
public void process(ChannelMsg channelMsg) {
final IPDto ipDto = channelMsg.toObject(IPDto.class);
String ip = ipDto.getIp();
switch (channelMsg.getOperation()) {
case "add":
log.info("添加IP黑名单ip:{}", ip);
add(ip);
break;
case "delete":
log.info("移除IP黑名单ip:{}", ip);
remove(ip);
break;
default:
}
}
@Data
private static class IPDto {
private String ip;
}
}

View File

@@ -1,58 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.sop.gateway.entity.IsvDetailDTO;
import com.gitee.sop.gateway.mapper.IsvInfoMapper;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.IsvDefinition;
import com.gitee.sop.gatewaycommon.secret.CacheIsvManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 六如
*/
@Slf4j
@Service
public class DbIsvManager extends CacheIsvManager {
public DbIsvManager() {
ApiConfig.getInstance().setIsvManager(this);
}
@Autowired
private IsvInfoMapper isvInfoMapper;
@Override
public void load() {
List<IsvDetailDTO> isvInfoList = isvInfoMapper.listIsvDetail();
isvInfoList
.forEach(isvInfo -> {
IsvDefinition isvDefinition = new IsvDefinition();
BeanUtils.copyProperties(isvInfo, isvDefinition);
this.getIsvCache().put(isvDefinition.getAppKey(), isvDefinition);
});
}
@Override
public void process(ChannelMsg channelMsg) {
final IsvDefinition isvDefinition = channelMsg.toObject(IsvDefinition.class);
switch (channelMsg.getOperation()) {
case "update":
log.info("更新ISV信息isvDefinition:{}", isvDefinition);
update(isvDefinition);
break;
case "remove":
log.info("删除ISVisvDefinition:{}", isvDefinition);
remove(isvDefinition.getAppKey());
break;
default:
}
}
}

View File

@@ -1,149 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.alibaba.fastjson.JSON;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.entity.IsvInfo;
import com.gitee.sop.gateway.entity.PermIsvRole;
import com.gitee.sop.gateway.entity.PermRolePermission;
import com.gitee.sop.gateway.mapper.IsvInfoMapper;
import com.gitee.sop.gateway.mapper.PermIsvRoleMapper;
import com.gitee.sop.gateway.mapper.PermRolePermissionMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.IsvRoutePermission;
import com.gitee.sop.gatewaycommon.manager.DefaultIsvRoutePermissionManager;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
/**
* 从数据库中读取路由权限信息
*
* @author 六如
*/
@Slf4j
@Service
public class DbIsvRoutePermissionManager extends DefaultIsvRoutePermissionManager {
@Autowired
Environment environment;
@Autowired
PermIsvRoleMapper permIsvRoleMapper;
@Autowired
PermRolePermissionMapper permRolePermissionMapper;
@Autowired
IsvInfoMapper isvInfoMapper;
@Override
public void load() {
// key: appKey, value: roleCodeList
Map<String, List<String>> appKeyRoleCodeMap = this.getIsvRoleCode();
for (Map.Entry<String, List<String>> entry : appKeyRoleCodeMap.entrySet()) {
this.loadIsvRoutePermission(entry.getKey(), entry.getValue());
}
}
public void loadIsvRoutePermission(String appKey, List<String> roleCodeList) {
Collections.sort(roleCodeList);
List<String> routeIdList = this.getRouteIdList(roleCodeList);
String roleCodeListMd5 = DigestUtils.md5Hex(JSON.toJSONString(routeIdList));
IsvRoutePermission isvRoutePermission = new IsvRoutePermission();
isvRoutePermission.setAppKey(appKey);
isvRoutePermission.setRouteIdList(routeIdList);
isvRoutePermission.setRouteIdListMd5(roleCodeListMd5);
this.update(isvRoutePermission);
}
/**
* 获取ISV对应的角色
* @return 返回ISV角色信息keyappIdvalue角色code列表
*/
public Map<String, List<String>> getIsvRoleCode() {
Query query = new Query();
List<PermIsvRole> permIsvRoles = permIsvRoleMapper.list(query);
Map<String, List<String>> appKeyRoleCodeMap = permIsvRoles.stream()
.map(permIsvRole -> {
IsvInfo isvInfo = isvInfoMapper.getById(permIsvRole.getIsvId());
if (isvInfo == null) {
return null;
}
IsvRole isvRole = new IsvRole();
isvRole.appKey = isvInfo.getAppKey();
isvRole.roleCode = permIsvRole.getRoleCode();
return isvRole;
})
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(IsvRole::getAppKey,
mapping(IsvRole::getRoleCode, toList()))
);
return appKeyRoleCodeMap;
}
/**
* 获取角色对应的路由
*
* @param roleCodeList
* @return
*/
public List<String> getRouteIdList(List<String> roleCodeList) {
if (CollectionUtils.isEmpty(roleCodeList)) {
return Collections.emptyList();
}
Query query = new Query();
query.in("role_code", roleCodeList);
List<PermRolePermission> rolePermissionList = permRolePermissionMapper.list(query);
return rolePermissionList.stream()
.map(PermRolePermission::getRouteId)
.sorted()
.collect(Collectors.toList());
}
@Override
public void process(ChannelMsg channelMsg) {
final IsvRoutePermission isvRoutePermission = channelMsg.toObject(IsvRoutePermission.class);
switch (channelMsg.getOperation()) {
case "reload":
log.info("重新加载路由权限信息isvRoutePermission:{}", isvRoutePermission);
try {
load();
} catch (Exception e) {
log.error("重新加载路由权限失败, channelMsg:{}", channelMsg, e);
}
break;
case "update":
log.info("更新ISV路由权限信息isvRoutePermission:{}", isvRoutePermission);
update(isvRoutePermission);
break;
case "remove":
log.info("删除ISV路由权限信息isvRoutePermission:{}", isvRoutePermission);
remove(isvRoutePermission.getAppKey());
break;
default:
}
}
@Data
static class IsvRole {
private String appKey;
private String roleCode;
}
}

View File

@@ -1,60 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.entity.ConfigLimit;
import com.gitee.sop.gateway.mapper.ConfigLimitMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import com.gitee.sop.gatewaycommon.manager.DefaultLimitConfigManager;
import com.gitee.sop.gatewaycommon.util.MyBeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 限流配置管理
* @author 六如
*/
@Slf4j
@Service
public class DbLimitConfigManager extends DefaultLimitConfigManager {
@Autowired
ConfigLimitMapper configLimitMapper;
@Override
public void load(String serviceId) {
Query query = new Query();
if (StringUtils.isNotBlank(serviceId)) {
query.eq("service_id", serviceId);
}
configLimitMapper.list(query)
.forEach(this::putVal);
}
protected void putVal(ConfigLimit object) {
ConfigLimitDto configLimitDto = new ConfigLimitDto();
MyBeanUtil.copyPropertiesIgnoreNull(object, configLimitDto);
this.update(configLimitDto);
}
@Override
public void process(ChannelMsg channelMsg) {
final ConfigLimitDto configLimitDto = channelMsg.toObject(ConfigLimitDto.class);
switch (channelMsg.getOperation()) {
case "reload":
log.info("重新加载限流配置信息configLimitDto:{}", configLimitDto);
load(configLimitDto.getServiceId());
break;
case "update":
log.info("更新限流配置信息configLimitDto:{}", configLimitDto);
update(configLimitDto);
break;
default:
}
}
}

View File

@@ -1,64 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.sop.gateway.mapper.MonitorInfoErrorMapper;
import com.gitee.sop.gateway.mapper.MonitorInfoMapper;
import com.gitee.sop.gatewaycommon.monitor.MonitorDTO;
import com.gitee.sop.gatewaycommon.monitor.MonitorErrorMsg;
import com.gitee.sop.gatewaycommon.monitor.RouteErrorCount;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author 六如
*/
@Service
public class DbMonitorInfoManager {
@Autowired
private MonitorInfoMapper monitorInfoMapper;
@Autowired
private MonitorInfoErrorMapper monitorInfoErrorMapper;
@Value("${sop.monitor.error-count-capacity:50}")
int limitCount;
public void saveMonitorInfoBatch(List<MonitorDTO> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
monitorInfoMapper.saveMonitorInfoBatch(list);
this.saveMonitorInfoErrorBatch(list);
}
private void saveMonitorInfoErrorBatch(List<MonitorDTO> list) {
List<RouteErrorCount> routeErrorCounts = monitorInfoErrorMapper.listRouteErrorCountAll();
// 路由id对应的错误次数keyrouteIdvalue错误次数
Map<String, Integer> routeErrorCountsMap = routeErrorCounts.stream()
.collect(Collectors.toMap(RouteErrorCount::getRouteId, RouteErrorCount::getCount));
List<MonitorErrorMsg> monitorErrorMsgList = list.stream()
.filter(monitorDTO -> CollectionUtils.isNotEmpty(monitorDTO.getErrorMsgList()))
.flatMap(monitorDTO -> {
int limit = limitCount - routeErrorCountsMap.getOrDefault(monitorDTO.getRouteId(), 0);
// 容量已满
if (limit <= 0) {
return null;
}
// 截取剩余
return monitorDTO.getErrorMsgList().stream().limit(limit);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(monitorErrorMsgList)) {
monitorInfoErrorMapper.saveMonitorInfoErrorBatch(monitorErrorMsgList);
}
}
}

View File

@@ -1,51 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.gitee.sop.gateway.mapper.ConfigRouteMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import com.gitee.sop.gatewaycommon.manager.DefaultRouteConfigManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 六如
*/
@Slf4j
@Service
public class DbRouteConfigManager extends DefaultRouteConfigManager {
@Autowired
private ConfigRouteMapper configRouteMapper;
@Autowired
private Environment environment;
@Override
public void load(String serviceId) {
List<RouteConfig> routeConfigs = StringUtils.isBlank(serviceId) ? configRouteMapper.listAllRouteConfig()
: configRouteMapper.listRouteConfig(serviceId);
routeConfigs.forEach(this::save);
}
@Override
public void process(ChannelMsg channelMsg) {
final RouteConfig routeConfig = channelMsg.toObject(RouteConfig.class);
switch (channelMsg.getOperation()) {
case "reload":
log.info("重新加载路由配置信息routeConfigDto:{}", routeConfig);
load(null);
break;
case "update":
log.info("更新路由配置信息routeConfigDto:{}", routeConfig);
update(routeConfig);
break;
default:
}
}
}

View File

@@ -1,86 +0,0 @@
package com.gitee.sop.gateway.manager;
import com.alibaba.fastjson.JSON;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.entity.ConfigServiceRoute;
import com.gitee.sop.gateway.mapper.ConfigServiceRouteMapper;
import com.gitee.sop.gateway.mapper.SystemLockMapper;
import com.gitee.sop.gatewaycommon.bean.InstanceDefinition;
import com.gitee.sop.gatewaycommon.bean.ServiceRouteInfo;
import com.gitee.sop.gatewaycommon.route.RoutesProcessor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author 六如
*/
@Slf4j
@Component
public class DbRoutesProcessor implements RoutesProcessor {
@Autowired
private ConfigServiceRouteMapper configServiceRouteMapper;
@Autowired
private SystemLockMapper systemLockMapper;
@Override
public void removeAllRoutes(String serviceId) {
// 删除serviceId下所有的路由
Query delServiceQuery = new Query().eq("service_id", serviceId);
configServiceRouteMapper.deleteByQuery(delServiceQuery);
}
@Transactional(rollbackFor = Exception.class)
@Override
public synchronized void saveRoutes(ServiceRouteInfo serviceRouteInfo, InstanceDefinition instance) {
// 抢锁,没抢到阻塞在这里
systemLockMapper.lock();
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
int result = systemLockMapper.insert(time + serviceRouteInfo.getMd5());
// 抢到锁,插入失败,表示其它实例已经处理完毕,这里直接返回
if (result == 0) {
return;
}
log.info("保存路由信息到数据库instance: {}", instance);
String serviceId = serviceRouteInfo.getServiceId();
List<ConfigServiceRoute> configServiceRoutes = serviceRouteInfo
.getRouteDefinitionList()
.parallelStream()
.map(routeDefinition -> {
ConfigServiceRoute configServiceRoute = new ConfigServiceRoute();
configServiceRoute.setId(routeDefinition.getId());
configServiceRoute.setName(routeDefinition.getName());
configServiceRoute.setVersion(routeDefinition.getVersion());
configServiceRoute.setUri(routeDefinition.getUri());
configServiceRoute.setPath(routeDefinition.getPath());
configServiceRoute.setFilters(JSON.toJSONString(routeDefinition.getFilters()));
configServiceRoute.setPredicates(JSON.toJSONString(routeDefinition.getPredicates()));
configServiceRoute.setIgnoreValidate((byte) routeDefinition.getIgnoreValidate());
configServiceRoute.setMergeResult((byte) routeDefinition.getMergeResult());
configServiceRoute.setStatus((byte) routeDefinition.getStatus());
configServiceRoute.setPermission((byte) routeDefinition.getPermission());
configServiceRoute.setOrderIndex(routeDefinition.getOrder());
configServiceRoute.setNeedToken((byte)routeDefinition.getNeedToken());
configServiceRoute.setServiceId(serviceId);
return configServiceRoute;
})
.collect(Collectors.toList());
// 删除serviceId下所有的路由
this.removeAllRoutes(serviceId);
if (CollectionUtils.isNotEmpty(configServiceRoutes)) {
// 批量保存
configServiceRouteMapper.saveBatch(configServiceRoutes);
}
}
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigGrayInstance;
/**
* @author 六如
*/
public interface ConfigGrayInstanceMapper extends CrudMapper<ConfigGrayInstance, Long> {
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigGray;
/**
* @author 六如
*/
public interface ConfigGrayMapper extends CrudMapper<ConfigGray, Long> {
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigLimit;
/**
* @author 六如
*/
public interface ConfigLimitMapper extends CrudMapper<ConfigLimit, Long> {
}

View File

@@ -1,12 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigRouteBase;
/**
* @author 六如
*/
public interface ConfigRouteBaseMapper extends CrudMapper<ConfigRouteBase, Long> {
}

View File

@@ -1,27 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 六如
*/
@Mapper
public interface ConfigRouteMapper {
@Select("SELECT t.id AS routeId, t2.status " +
"FROM config_service_route t " +
"LEFT JOIN config_route_base t2 ON t.id=t2.route_id " +
"WHERE t.service_id=#{serviceId}")
List<RouteConfig> listRouteConfig(@Param("serviceId") String serviceId);
@Select("SELECT t.id AS routeId, t2.status " +
"FROM config_service_route t " +
"LEFT JOIN config_route_base t2 ON t.id=t2.route_id ")
List<RouteConfig> listAllRouteConfig();
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigServiceRoute;
/**
* @author 六如
*/
public interface ConfigServiceRouteMapper extends CrudMapper<ConfigServiceRoute, String> {
}

View File

@@ -1,22 +0,0 @@
package com.gitee.sop.gateway.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* IP黑名单
* @author 六如
*/
@Mapper
public interface IPBlacklistMapper {
/**
* 获取所有IP
* @return
*/
@Select("SELECT ip FROM config_ip_blacklist")
List<String> listAllIP();
}

View File

@@ -1,30 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.IsvDetailDTO;
import com.gitee.sop.gateway.entity.IsvInfo;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 六如
*/
public interface IsvInfoMapper extends CrudMapper<IsvInfo, Long> {
/**
* 获取所有的isv信息
* @return 所有的isv信息
*/
@Select("SELECT " +
" t.app_key appKey " +
" ,t.status " +
" ,t2.sign_type signType " +
" ,t2.secret " +
" ,t2.public_key_isv publicKeyIsv " +
" ,t2.private_key_platform privateKeyPlatform " +
"FROM isv_info t " +
"INNER JOIN isv_keys t2 ON t.app_key = t2.app_key")
List<IsvDetailDTO> listIsvDetail();
}

View File

@@ -1,36 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.MonitorInfoError;
import com.gitee.sop.gatewaycommon.monitor.MonitorErrorMsg;
import com.gitee.sop.gatewaycommon.monitor.RouteErrorCount;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @author 六如
*/
public interface MonitorInfoErrorMapper extends CrudMapper<MonitorInfoError, Long> {
@Update("UPDATE monitor_info_error " +
"SET is_deleted=0 " +
",count=count + 1 " +
"WHERE route_id=#{routeId} AND error_id=#{errorId}")
int updateError(@Param("routeId") String routeId,@Param("errorId") String errorId);
int saveMonitorInfoErrorBatch(@Param("list") List<MonitorErrorMsg> list);
@Select("SELECT route_id routeId, count(*) `count` FROM monitor_info_error \n" +
"WHERE is_deleted=0 \n" +
"GROUP BY route_id")
List<RouteErrorCount> listRouteErrorCount();
@Select("SELECT route_id routeId, count(*) `count` FROM monitor_info_error \n" +
"WHERE is_deleted=0 \n" +
"GROUP BY route_id")
List<RouteErrorCount> listRouteErrorCountAll();
}

View File

@@ -1,39 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.MonitorInfo;
import com.gitee.sop.gatewaycommon.monitor.MonitorDTO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @author 六如
*/
public interface MonitorInfoMapper extends CrudMapper<MonitorInfo, Long> {
/**
* 更新监控状态
*
* @return 返回影响行数
*/
@Update("UPDATE monitor_info " +
"set max_time=case when max_time < #{maxTime} then #{maxTime} else max_time end " +
",min_time=case when min_time > #{minTime} then #{minTime} else min_time end " +
",total_request_count=total_request_count + #{totalRequestCount} " +
",total_time=total_time + #{totalTime} " +
",success_count=success_count + #{successCount} " +
",error_count=error_count + #{errorCount} " +
"where route_id=#{routeId}")
int updateMonitorInfo(MonitorDTO monitorDTO);
/**
* 批量插入监控数据
* @param list 监控数据
* @return 返回影响行数
*/
int saveMonitorInfoBatch(@Param("list") List<MonitorDTO> list);
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.PermIsvRole;
/**
* @author 六如
*/
public interface PermIsvRoleMapper extends CrudMapper<PermIsvRole, Long> {
}

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.PermRolePermission;
/**
* @author 六如
*/
public interface PermRolePermissionMapper extends CrudMapper<PermRolePermission, Long> {
}

View File

@@ -1,27 +0,0 @@
package com.gitee.sop.gateway.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.ResultType;
import org.apache.ibatis.annotations.Select;
/**
* @author 六如
*/
@Mapper
public interface SystemLockMapper {
/**
* 插入唯一值
* @param content 唯一值
* @return 1返回成功0插入失败
*/
@Insert("INSERT IGNORE INTO system_lock(content) VALUES (#{content})")
@ResultType(int.class)
int insert(@Param("content") String content);
@Select("SELECT id FROM system_lock WHERE id=1 FOR UPDATE")
@ResultType(long.class)
long lock();
}

View File

@@ -0,0 +1,132 @@
package com.gitee.sop.gateway.message;
/**
* 网关错误定义
* @author 六如
*/
public enum ErrorEnum {
/** 成功 */
SUCCESS(Codes.CODE_SUCCESS, ""),
/** 服务暂不可用 */
ISP_UNKNOWN_ERROR(Codes.CODE_UNKNOWN, "isp.unknown-error"),
/** 微服务未知错误 */
ISP_SERVICE_UNKNOWN_ERROR(Codes.CODE_UNKNOWN, "isp.service-unknown-error"),
/** 服务不可用,路由被禁用 */
ISP_API_DISABLED(Codes.CODE_UNKNOWN, "isp.service-not-available"),
/** 网关响应超时 */
ISP_GATEWAY_RESPONSE_TIMEOUT(Codes.CODE_UNKNOWN, "isp.gateway-response-timeout"),
/** 限流处理 */
ISV_REQUEST_LIMIT(Codes.CODE_UNKNOWN, "isv.service-busy"),
/** 无效的访问令牌 */
AOP_INVALID_AUTH_TOKEN(Codes.CODE_AUTH, "aop.invalid-auth-token"),
/** 访问令牌已过期 */
AOP_AUTH_TOKEN_TIME_OUT(Codes.CODE_AUTH, "aop.auth-token-time-out"),
/** 无效的应用授权令牌 */
AOP_INVALID_APP_AUTH_TOKEN(Codes.CODE_AUTH, "aop.invalid-app-auth-token"),
/** 商户未授权当前接口 */
AOP_INVALID_APP_AUTH_TOKEN_NO_API(Codes.CODE_AUTH, "aop.invalid-app-auth-token-no-api"),
/** 应用授权令牌已过期 */
AOP_APP_AUTH_TOKEN_TIME_OUT(Codes.CODE_AUTH, "aop.app-auth-token-time-out"),
/** 商户未签约任何产品 */
AOP_NO_PRODUCT_REG_BY_PARTNER(Codes.CODE_AUTH, "aop.no-product-reg-by-partner"),
/** 缺少方法名参数 */
ISV_MISSING_METHOD(Codes.CODE_MISSING, "isv.missing-method"),
/** 缺少签名参数 */
ISV_MISSING_SIGNATURE(Codes.CODE_MISSING, "isv.missing-signature"),
/** 缺少签名类型参数 */
ISV_MISSING_SIGNATURE_TYPE(Codes.CODE_MISSING, "isv.missing-signature-type"),
/** 缺少签名配置 */
ISV_MISSING_SIGNATURE_KEY(Codes.CODE_MISSING, "isv.missing-signature-key"),
/** 缺少appId参数 */
ISV_MISSING_APP_ID(Codes.CODE_MISSING, "isv.missing-app-id"),
/** 缺少时间戳参数 */
ISV_MISSING_TIMESTAMP(Codes.CODE_MISSING, "isv.missing-timestamp"),
/** 缺少版本参数 */
ISV_MISSING_VERSION(Codes.CODE_MISSING, "isv.missing-version"),
/** 解密出错, 未指定加密算法 */
ISV_DECRYPTION_ERROR_MISSING_ENCRYPT_TYPE(Codes.CODE_MISSING, "isv.decryption-error-missing-encrypt-type"),
/** 参数无效 */
ISV_INVALID_PARAMETER(Codes.CODE_INVALID, "isv.invalid-parameter"),
/** 参数不正确 */
ISV_ERROR_PARAMETER(Codes.CODE_INVALID, "isv.error-parameter"),
/** 文件上传失败 */
ISV_UPLOAD_FAIL(Codes.CODE_INVALID, "isv.upload-fail"),
/** 文件扩展名无效 */
ISV_INVALID_FILE_EXTENSION(Codes.CODE_INVALID, "isv.invalid-file-extension"),
/** 文件大小无效 */
ISV_INVALID_FILE_SIZE(Codes.CODE_INVALID, "isv.invalid-file-size"),
/** 不存在的方法名 */
ISV_INVALID_METHOD(Codes.CODE_INVALID, "isv.invalid-method"),
/** 无效的数据格式 */
ISV_INVALID_FORMAT(Codes.CODE_INVALID, "isv.invalid-format"),
/** 无效的签名类型 */
ISV_INVALID_SIGNATURE_TYPE(Codes.CODE_INVALID, "isv.invalid-signature-type"),
/** 无效签名 */
ISV_INVALID_SIGNATURE(Codes.CODE_INVALID, "isv.invalid-signature"),
/** 无效的加密类型 */
ISV_INVALID_ENCRYPT_TYPE(Codes.CODE_INVALID, "isv.invalid-encrypt-type"),
/** 解密异常 */
ISV_INVALID_ENCRYPT(Codes.CODE_INVALID, "isv.invalid-encrypt"),
/** 无效的appId参数 */
ISV_INVALID_APP_ID(Codes.CODE_INVALID, "isv.invalid-app-id"),
/** 非法的时间戳参数 */
ISV_INVALID_TIMESTAMP(Codes.CODE_INVALID, "isv.invalid-timestamp"),
/** 字符集错误 */
ISV_INVALID_CHARSET(Codes.CODE_INVALID, "isv.invalid-charset"),
/** 摘要错误 */
ISV_INVALID_DIGEST(Codes.CODE_INVALID, "isv.invalid-digest"),
/** 解密出错,不支持的加密算法 */
ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_TYPE(Codes.CODE_INVALID, "isv.decryption-error-not-valid-encrypt-type"),
/** 解密出错, 未配置加密密钥或加密密钥格式错误 */
ISV_DECRYPTION_ERROR_NOT_VALID_ENCRYPT_KEY(Codes.CODE_INVALID, "isv.decryption-error-not-valid-encrypt-key"),
/** 解密出错,未知异常 */
ISV_DECRYPTION_ERROR_UNKNOWN(Codes.CODE_INVALID, "isv.decryption-error-unknown"),
/** 验签出错, 未配置对应签名算法的公钥或者证书 */
ISV_MISSING_SIGNATURE_CONFIG(Codes.CODE_INVALID, "isv.missing-signature-config"),
/** 本接口不支持第三方代理调用 */
ISV_NOT_SUPPORT_APP_AUTH(Codes.CODE_INVALID, "isv.not-support-app-auth"),
/** 可疑的攻击请求 */
ISV_SUSPECTED_ATTACK(Codes.CODE_INVALID, "isv.suspected-attack"),
/** 无效的content-type */
ISV_INVALID_CONTENT_TYPE(Codes.CODE_INVALID, "isv.invalid-content-type"),
/** 业务处理失败 */
BIZ_ERROR(Codes.CODE_BIZ, ""),
/** 请检查配置的账户是否有当前接口权限 */
ISV_INSUFFICIENT_ISV_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-isv-permissions"),
/** 代理的商户没有当前接口权限 */
ISV_INSUFFICIENT_USER_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-user-permissions"),
/** 没有当前接口权限 */
ISV_ROUTE_NO_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.route-no-permissions"),
/** 禁止访问 */
ISV_ACCESS_FORBIDDEN(Codes.CODE_ISV_PERM, "isv.access-forbidden"),
/** 禁止IP访问 */
ISV_IP_FORBIDDEN(Codes.CODE_ISV_PERM, "isv.ip-forbidden"),
;
private final ErrorMeta errorMeta;
ErrorEnum(String code, String subCode) {
this.errorMeta = new ErrorMeta("open.error_", code, subCode);
}
public ErrorMeta getErrorMeta() {
return errorMeta;
}
private static class Codes {
public static final String CODE_SUCCESS = "10000";
public static final String CODE_AUTH = "20001";
public static final String CODE_MISSING = "40001";
public static final String CODE_INVALID = "40002";
public static final String CODE_BIZ = "40004";
public static final String CODE_ISV_PERM = "40006";
public static final String CODE_UNKNOWN = "20000";
}
}

View File

@@ -0,0 +1,134 @@
package com.gitee.sop.gateway.message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* 负责构建错误消息
*
* @author 六如
*/
@Slf4j
public class ErrorFactory {
private static final String ZH = "zh";
private static final String SYS_ERR = "系统错误";
private static final String I18N_OPEN_ERROR = "i18n/open/error";
public static final String UNDERLINE = "_";
private static Set<String> noModuleCache = new HashSet<>();
private static Map<String, IError> errorCache = new HashMap<>(64);
private static List<Locale> localeList = Arrays.asList(Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE);
private ErrorFactory() {
}
/**
* 错误信息的国际化信息
*/
private static MessageSourceAccessor errorMessageSourceAccessor;
/**
* 设置国际化资源信息
*/
public static void initMessageSource(List<String> isvModules) {
HashSet<String> baseNamesSet = new HashSet<>();
baseNamesSet.add(I18N_OPEN_ERROR);
if (!CollectionUtils.isEmpty(isvModules)) {
baseNamesSet.addAll(isvModules);
}
String[] totalBaseNames = baseNamesSet.toArray(new String[0]);
log.info("加载错误码国际化资源:{}", StringUtils.arrayToCommaDelimitedString(totalBaseNames));
ResourceBundleMessageSource bundleMessageSource = new ResourceBundleMessageSource();
bundleMessageSource.setBasenames(totalBaseNames);
MessageSourceAccessor messageSourceAccessor = new MessageSourceAccessor(bundleMessageSource);
setErrorMessageSourceAccessor(messageSourceAccessor);
}
/**
* 通过ErrorMetaLocaleparams构建国际化错误消息
*
* @param errorMeta 错误信息
* @param locale 本地化
* @param params 参数
* @return 如果没有配置国际化消息则直接返回errorMeta中的信息
*/
public static IError getError(ErrorMeta errorMeta, Locale locale, Object... params) {
if (locale == null) {
locale = Locale.SIMPLIFIED_CHINESE;
}
String localeStr = locale.toString();
if (localeStr.startsWith(ZH)) {
locale = Locale.SIMPLIFIED_CHINESE;
}
if (!localeList.contains(locale)) {
locale = Locale.ENGLISH;
}
String key = errorMeta.getModulePrefix() + errorMeta.getCode() + errorMeta.getSubCode() + locale.toString();
Assert.notNull(locale, "未设置Locale");
String modulePrefix = errorMeta.getModulePrefix();
String code = errorMeta.getCode();
// open.error_20000=Service is temporarily unavailable
String msg = getErrorMessage(modulePrefix + code, locale);
String subCode = errorMeta.getSubCode();
// open.error_20000_isp.unknown-error=Service is temporarily unavailable
String subMsg = getErrorMessage(modulePrefix + code + UNDERLINE + subCode, locale, params);
if (StringUtils.isEmpty(msg)) {
msg = SYS_ERR;
}
if (StringUtils.isEmpty(subMsg)) {
subMsg = SYS_ERR;
}
// solution暂未实现如果要实现可以这样配置
// open.error_20000_isp.unknown-error_solution=Service is temporarily unavailable
// <code>String solution = getErrorMessage(modulePrefix + code + UNDERLINE + subCode + "_solution", locale, params);</code>
return new ErrorImpl(code, msg, subCode, subMsg, null);
}
public static void setErrorMessageSourceAccessor(MessageSourceAccessor errorMessageSourceAccessor) {
ErrorFactory.errorMessageSourceAccessor = errorMessageSourceAccessor;
}
/**
* 返回本地化信息
*
* @param module 错误模块
* @param locale 本地化
* @param params 参数
* @return 返回信息
*/
public static String getErrorMessage(String module, Locale locale, Object... params) {
if (noModuleCache.contains(module)) {
return null;
}
try {
return errorMessageSourceAccessor.getMessage(module, params, locale);
} catch (Exception e) {
noModuleCache.add(module);
return null;
}
}
}

View File

@@ -0,0 +1,24 @@
package com.gitee.sop.gateway.message;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class ErrorImpl implements IError {
private String code;
private String msg;
private String sub_code;
private String sub_msg;
private String solution;
public ErrorImpl(String code, String msg, String sub_code, String sub_msg, String solution) {
this.code = code;
this.msg = msg;
this.sub_code = sub_code;
this.sub_msg = sub_msg;
this.solution = solution;
}
}

View File

@@ -0,0 +1,34 @@
package com.gitee.sop.gateway.message;
import lombok.Getter;
import java.util.Locale;
/**
* 错误对象
*
* @author 六如
*/
@Getter
public class ErrorMeta {
private final String modulePrefix;
private final String code;
private final String subCode;
public ErrorMeta(String modulePrefix, String code, String subCode) {
this.modulePrefix = modulePrefix;
this.code = code;
this.subCode = subCode;
}
public IError getError(Locale locale, Object... params) {
return ErrorFactory.getError(this, locale, params);
}
@Override
public String toString() {
return modulePrefix + code + "_" + subCode;
}
}

View File

@@ -0,0 +1,48 @@
package com.gitee.sop.gateway.message;
/**
* 定义错误返回
* code返回码
* msg返回码描述
* sub_code明细返回码
* sub_msg明细返回码描述
* 解决方案
* @author 六如
*/
public interface IError {
/**
* 获取网关状态码
*
* @return 返回状态码
*/
String getCode();
/**
* 获取网关错误信息
*
* @return 返回错误信息
*/
String getMsg();
/**
* sub_code明细返回码
* @return sub_code明细返回码
*/
String getSub_code();
/**
* sub_msg明细返回码描述
* @return sub_msg明细返回码描述
*/
String getSub_msg();
/**
* 解决方案
* @return 解决方案
*/
String getSolution();
}

View File

@@ -0,0 +1,126 @@
package com.gitee.sop.gateway.request;
import com.alibaba.fastjson2.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* 请求参数名定义
* <pre>
* 参数 类型 是否必填 最大长度 描述 示例值
* app_id String 是 32 支付宝分配给开发者的应用ID 2014072300007148
* method String 是 128 接口名称 alipay.trade.fastpay.refund.query
* format String 否 40 仅支持JSON JSON
* charset String 是 10 请求使用的编码格式如utf-8,gbk,gb2312等 utf-8
* sign_type String 是 10 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2 RSA2
* sign String 是 344 商户请求参数的签名串,详见签名 详见示例
* timestamp String 是 19 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" 2014-07-24 03:07:50
* version String 是 3 调用的接口版本固定为1.0 1.0
* notify_url String 否 256 支付宝服务器主动通知商户服务器里指定的页面http/https路径。 http://api.test.alipay.net/atinterface/receive_notify.htm
* app_auth_token String 否 40 详见应用授权概述
* biz_content String 是 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
*</pre>
* @author 六如
*/
@Data
public class ApiRequest implements Serializable {
private static final long serialVersionUID = 1815097687653555654L;
/**
* 分配给开发者的应用ID
*
* @mock 2014072300007148
*/
@NotBlank(message = "应用ID不能为空")
@Length(max = 32)
private String appId;
/**
* 接口名称
*
* @mock shop.goods.get
*/
@NotBlank(message = "method不能为空")
@Length(max = 128)
private String method;
/**
* 仅支持JSON
*
* @mock json
*/
@NotBlank(message = "format不能为空")
@Length(max = 40)
private String format;
/**
* 请求使用的编码格式如utf-8,gbk,gb2312等
*
* @mock utf-8
*/
@Length(max = 10)
private String charset = "utf-8";
/**
* 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2
*
* @mock RSA2
*/
@NotBlank(message = "sign_type不能为空")
@Length(max = 10)
private String signType;
/**
* 商户请求参数的签名串,详见签名
*/
@NotBlank(message = "sign不能为空")
private String sign;
/**
* 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss"
*
* @mock 2014-07-24 03:07:50
*/
@NotBlank(message = "timestamp不能为空")
@Length(max = 19)
private String timestamp;
/**
* 调用的接口版本固定为1.0
*
* @mock 1.0
*/
@NotBlank(message = "version不能为空")
@Length(max = 10)
private String version;
/**
* 支付宝服务器主动通知商户服务器里指定的页面http/https路径
*
* @mock http://ww.xx.com/callback
*/
@Length(max = 256)
private String notifyUrl;
/**
* 授权token,详见应用授权概述
*/
@Length(max = 64)
private String appAuthToken;
/**
* 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
*/
@NotBlank(message = "biz_content不能为空")
private String bizContent;
@JsonIgnore
@JSONField(serialize = false)
public String takeNameVersion() {
return method + version;
}
}

View File

@@ -0,0 +1,36 @@
package com.gitee.sop.gateway.request;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.Locale;
/**
* @author 六如
*/
@Builder
@Getter
@Setter
public class ApiRequestContext {
/**
* 请求参数
*/
private ApiRequest apiRequest;
/**
* 本地语言
*/
private Locale locale;
/**
* 客户端ip
*/
private String ip;
/**
* traceId
*/
private String traceId;
/**
* 上传文件
*/
private UploadContext uploadContext;
}

View File

@@ -0,0 +1,74 @@
package com.gitee.sop.gateway.request;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.gitee.sop.gateway.common.SopConstants;
import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.util.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
/**
* @author 六如
*/
@Slf4j
public class ApiRequestContextFactory {
private static final String CONTENT_TYPE = "content-type";
private static final String JSON_NAME = "json";
private static final String TEXT_NAME = "text";
private static final String MULTIPART = "multipart";
public static final String FORM = "form";
public static ApiRequestContext build(HttpServletRequest request, ApiConfig apiConfig) {
// get请求可能返回null
String contentType = request.getHeader(CONTENT_TYPE);
if (contentType == null) {
contentType = "";
}
ApiRequest apiRequest = new ApiRequest();
String ip = RequestUtil.getIP(request);
byte[] body;
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException e) {
log.error("获取请求体失败", e);
body = new byte[0];
}
JSONObject params = null;
UploadContext uploadContext = null;
String contentTypeStr = contentType.toLowerCase();
// 如果是json方式提交
if (StringUtils.containsAny(contentTypeStr, JSON_NAME, TEXT_NAME)) {
params = JSON.parseObject(body);
} else if (StringUtils.containsIgnoreCase(contentTypeStr, MULTIPART)) {
// 如果是文件上传请求
RequestUtil.UploadInfo uploadInfo = RequestUtil.getUploadInfo(request);
params = uploadInfo.getApiParam();
uploadContext = uploadInfo.getUploadContext();
} else if (StringUtils.containsIgnoreCase(contentTypeStr, FORM)) {
// APPLICATION_FORM_URLENCODED请求
params = RequestUtil.parseQuerystring(new String(body, SopConstants.CHARSET_UTF8));
} else {
// get请求,参数跟在url后面
params = RequestUtil.parseParameterMap(request.getParameterMap());
}
if (params != null) {
apiRequest = params.toJavaObject(ApiRequest.class);
}
return ApiRequestContext.builder()
.apiRequest(apiRequest)
.locale(request.getLocale())
.ip(ip)
.uploadContext(uploadContext)
.traceId(UUID.randomUUID().toString().replace("-", ""))
.build();
}
}

View File

@@ -0,0 +1,46 @@
package com.gitee.sop.gateway.request;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 存放上传文件
*
* @author 六如
*/
public class ApiUploadContext implements UploadContext {
/**
* key: 表单name
*/
private Map<String, List<MultipartFile>> fileMap;
private List<MultipartFile> allFile;
public ApiUploadContext(Map<String, List<MultipartFile>> map) {
if (map == null) {
map = Collections.emptyMap();
}
this.fileMap = map;
this.allFile = new ArrayList<>();
map.values().forEach(list -> this.allFile.addAll(list));
}
@Override
public MultipartFile getFile(int index) {
return this.allFile.get(index);
}
@Override
public List<MultipartFile> getFile(String name) {
return fileMap.get(name);
}
@Override
public List<MultipartFile> getAllFile() {
return this.allFile;
}
}

View File

@@ -0,0 +1,26 @@
package com.gitee.sop.gateway.request;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 六如
*/
@Getter
@AllArgsConstructor
public enum RequestFormatEnum {
NONE(""),
JSON("json"),
XML("xml"),
;
private final String value;
public static RequestFormatEnum of(String value) {
for (RequestFormatEnum requestFormatEnum : RequestFormatEnum.values()) {
if (requestFormatEnum.value.equalsIgnoreCase(value)) {
return requestFormatEnum;
}
}
return NONE;
}
}

View File

@@ -0,0 +1,36 @@
package com.gitee.sop.gateway.request;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 获取上传文件
*
* @author 六如
*/
public interface UploadContext {
/**
* 根据索引获取上传文件,从0开始
*
* @param index
* @return 返回上传文件信息
*/
MultipartFile getFile(int index);
/**
* 根据表单名获取上传文件
*
* @param name
* 表单名称
* @return 返回上传文件信息
*/
List<MultipartFile> getFile(String name);
/**
* 获取所有的上传文件
*
* @return 返回所有的上传文件
*/
List<MultipartFile> getAllFile();
}

View File

@@ -0,0 +1,128 @@
package com.gitee.sop.gateway.response;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.message.ErrorMeta;
import com.gitee.sop.gateway.message.IError;
import lombok.Data;
import java.util.Locale;
/**
* 默认的结果封装类.
* <pre>
*
* xml返回结果:
* <response>
* <code>50</code>
* <msg>Remote service error</msg>
* <sub_code>isv.invalid-parameter</sub_code>
* <sub_msg>非法参数</sub_msg>
* </response>
* 成功情况:
* <response>
* <code>0</code>
* <msg>成功消息</msg>
* <data>
* ...返回内容
* </data>
* </response>
*
* json返回格式
* {
* "code":"50",
* "msg":"Remote service error",
* "sub_code":"isv.invalid-parameter",
* "sub_msg":"非法参数"
* }
* 成功情况:
* {
* "code":"0",
* "msg":"成功消息内容。。。",
* "data":{
* ...返回内容
* }
* }
* </pre>
* <p>
* 字段说明:
* code:网关异常码 <br>
* msg:网关异常信息 <br>
* sub_code:业务异常码 <br>
* sub_msg:业务异常信息 <br>
*
* @author 六如
*/
@Data
public class ApiResponse {
public static final String SUCCESS_CODE = "0";
public static final String SUCCESS_MSG = "success";
/**
* 网关异常码范围0~100 成功返回"0"
*/
private String code = SUCCESS_CODE;
/**
* 网关异常信息
*/
private String msg = "";
/**
* 业务异常码
*/
private String sub_code = "";
/**
* 业务异常信息
*/
private String sub_msg = "";
/**
* 返回对象
*/
private Object data;
public static ApiResponse success(Object data) {
ApiResponse apiResponse = new ApiResponse();
apiResponse.setCode(SUCCESS_CODE);
apiResponse.setMsg(SUCCESS_MSG);
apiResponse.setData(data);
return apiResponse;
}
public static ApiResponse error(ApiException e) {
ApiResponse apiResponse = new ApiResponse();
IError error = e.getError();
apiResponse.setCode(error.getCode());
apiResponse.setMsg(error.getMsg());
apiResponse.setSub_code(error.getSub_code());
apiResponse.setSub_msg(error.getSub_msg());
return apiResponse;
}
public static ApiResponse error(ErrorEnum errorEnum, Locale locale, String subMsg) {
ApiResponse apiResponse = new ApiResponse();
ErrorMeta errorMeta = errorEnum.getErrorMeta();
IError error = errorMeta.getError(locale);
apiResponse.setCode(error.getCode());
apiResponse.setMsg(error.getMsg());
apiResponse.setSub_code(error.getSub_code());
apiResponse.setSub_msg(subMsg);
return apiResponse;
}
public static ApiResponse error(ErrorEnum errorEnum, Locale locale) {
ApiResponse apiResponse = new ApiResponse();
ErrorMeta errorMeta = errorEnum.getErrorMeta();
IError error = errorMeta.getError(locale);
apiResponse.setCode(error.getCode());
apiResponse.setMsg(error.getMsg());
apiResponse.setSub_code(error.getSub_code());
apiResponse.setSub_msg(error.getSub_msg());
return apiResponse;
}
}

View File

@@ -0,0 +1,58 @@
package com.gitee.sop.gateway.service;
import org.apache.dubbo.common.config.ReferenceCache;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.utils.SimpleReferenceCache;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* dubbo泛化调用
*
* @author 六如
*/
@Service
public class GenericServiceInvoker implements InitializingBean {
private static final String TRUE = "true";
private ApplicationConfig applicationConfig;
@Value("${register.address:${register.type}://${register.host}")
private String registerAddress;
@Value("${spring.application.name}")
private String appName;
@Value("${generic.timeout:5000}")
private int timeout;
@Override
public void afterPropertiesSet() throws Exception {
RegistryConfig registryConfig = buildRegistryConfig();
applicationConfig = new ApplicationConfig();
applicationConfig.setName(appName + "-generic");
applicationConfig.setRegistry(registryConfig);
}
private RegistryConfig buildRegistryConfig() {
RegistryConfig config = new RegistryConfig();
config.setAddress(registerAddress);
return config;
}
public Object invoke(String interfaceName, String method, String[] parameterTypes, Object[] params) {
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
reference.setGeneric(TRUE);
reference.setApplication(applicationConfig);
reference.setInterface(interfaceName);
reference.setTimeout(timeout);
ReferenceCache referenceCache = SimpleReferenceCache.getCache();
GenericService genericService = referenceCache.get(reference);
return genericService.$invoke(method, parameterTypes, params);
}
}

View File

@@ -0,0 +1,33 @@
package com.gitee.sop.gateway.service;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
import java.io.IOException;
/**
* 参数处理
*
* @author 六如
*/
public interface ParamExecutor<Req, Resp> {
/**
* 构建请求参数上下文
*
* @param request request
* @return 返回请求参数上下文
*/
ApiRequestContext build(Req request);
/**
* 结果返回写入
*
* @param apiRequestContext 请求参数上下文
* @param apiResponse 最终返回结果
* @param response response
* @throws IOException IOException
*/
void write(ApiRequestContext apiRequestContext, ApiResponse apiResponse, Resp response) throws IOException;
}

View File

@@ -0,0 +1,117 @@
package com.gitee.sop.gateway.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.gitee.sop.gateway.common.SopConstants;
import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.request.UploadContext;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.util.RequestUtil;
import com.gitee.sop.gateway.util.ResponseUtil;
import com.gitee.sop.support.dto.FileData;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* 请求参数默认实现
*
* @author 六如
*/
@Slf4j
public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, HttpServletResponse> {
private static final String CONTENT_TYPE = "content-type";
private static final String JSON_NAME = "json";
private static final String TEXT_NAME = "text";
private static final String MULTIPART = "multipart";
private static final String FORM = "form";
@Resource
private ApiConfig apiConfig;
@Override
public ApiRequestContext build(HttpServletRequest request) {
// get请求可能返回null
String contentType = request.getHeader(CONTENT_TYPE);
if (contentType == null) {
contentType = "";
}
ApiRequest apiRequest = new ApiRequest();
String ip = RequestUtil.getIP(request);
byte[] body;
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException e) {
log.error("获取请求体失败", e);
body = new byte[0];
}
JSONObject params = null;
UploadContext uploadContext = null;
String contentTypeStr = contentType.toLowerCase();
// 如果是json方式提交
if (StringUtils.containsAny(contentTypeStr, JSON_NAME, TEXT_NAME)) {
params = JSON.parseObject(body);
} else if (StringUtils.containsIgnoreCase(contentTypeStr, MULTIPART)) {
// 如果是文件上传请求
RequestUtil.UploadInfo uploadInfo = RequestUtil.getUploadInfo(request);
params = uploadInfo.getApiParam();
uploadContext = uploadInfo.getUploadContext();
} else if (StringUtils.containsIgnoreCase(contentTypeStr, FORM)) {
// APPLICATION_FORM_URLENCODED请求
params = RequestUtil.parseQuerystring(new String(body, SopConstants.CHARSET_UTF8));
} else {
// get请求,参数跟在url后面
params = RequestUtil.parseParameterMap(request.getParameterMap());
}
if (params != null) {
apiRequest = convertApiRequest(params);
}
return ApiRequestContext.builder()
.apiRequest(apiRequest)
.locale(request.getLocale())
.ip(ip)
.uploadContext(uploadContext)
.traceId(UUID.randomUUID().toString().replace("-", ""))
.build();
}
protected ApiRequest convertApiRequest(JSONObject jsonObject) {
ApiRequest apiRequest = new ApiRequest();
apiRequest.setAppId(jsonObject.getString(apiConfig.getAppIdName()));
apiRequest.setMethod(jsonObject.getString(apiConfig.getApiName()));
apiRequest.setFormat(jsonObject.getString(apiConfig.getFormatName()));
apiRequest.setCharset(jsonObject.getString(apiConfig.getCharsetName()));
apiRequest.setSignType(jsonObject.getString(apiConfig.getSignTypeName()));
apiRequest.setSign(jsonObject.getString(apiConfig.getSignName()));
apiRequest.setTimestamp(jsonObject.getString(apiConfig.getTimestampName()));
apiRequest.setVersion(jsonObject.getString(apiConfig.getVersionName()));
apiRequest.setNotifyUrl(jsonObject.getString(apiConfig.getNotifyUrlName()));
apiRequest.setAppAuthToken(jsonObject.getString(apiConfig.getAppAuthTokenName()));
apiRequest.setBizContent(jsonObject.getString(apiConfig.getBizContentName()));
return apiRequest;
}
@Override
public void write(ApiRequestContext apiRequestContext, ApiResponse apiResponse, HttpServletResponse response) throws IOException {
Object data = apiResponse.getData();
if (data instanceof FileData) {
FileData fileData = (FileData) data;
ResponseUtil.writerFile(fileData, response);
} else {
// 此处还可以判断charset,返回xml格式
ResponseUtil.writerText(apiRequestContext, apiResponse, response);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.gitee.sop.gateway.service;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
/**
* 接口路由
*
* @author 六如
*/
public interface RouteService {
/**
* 路由
*
* @param apiRequestContext 接口上下文
* @return 返回结果
*/
ApiResponse route(ApiRequestContext apiRequestContext);
}

View File

@@ -0,0 +1,208 @@
package com.gitee.sop.gateway.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.common.AttachmentNames;
import com.gitee.sop.gateway.common.ParamInfoDTO;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.exception.ExceptionExecutor;
import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.request.UploadContext;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
import com.gitee.sop.gateway.service.validate.Validator;
import com.gitee.sop.gateway.util.ClassUtil;
import com.gitee.sop.support.dto.CommonFileData;
import com.gitee.sop.support.dto.FileData;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.utils.ClassUtils;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcContextAttachment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 接口路由
*
* @author 六如
*/
@Slf4j
public class RouteServiceImpl implements RouteService {
@Resource
protected Validator validator;
@Resource
protected GenericServiceInvoker genericServiceInvoker;
@Resource
protected ExceptionExecutor exceptionExecutor;
@Autowired(required = false)
private List<RouteInterceptor> routeInterceptors;
@Override
public ApiResponse route(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
log.info("收到客户端请求, ip={}, apiRequest={}", apiRequestContext.getIp(), apiRequest);
try {
// 接口校验
ApiInfoDTO apiInfoDTO = validator.validate(apiRequestContext);
this.doPreRoute(apiRequestContext, apiInfoDTO);
// 微服务结果
Object result = doRoute(apiRequestContext, apiInfoDTO);
result = this.doAfterRoute(apiRequestContext, apiInfoDTO, result);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("接口请求报错, , ip={}, apiRequest={}", apiRequestContext.getIp(), apiRequest, e);
return exceptionExecutor.executeException(apiRequestContext, e);
}
}
protected Object doRoute(ApiRequestContext apiRequestContext, ApiInfoDTO apiInfo) {
setAttachment(apiRequestContext);
String paramInfo = apiInfo.getParamInfo();
List<ParamInfoDTO> paramInfoList = JSON.parseArray(paramInfo, ParamInfoDTO.class);
return genericServiceInvoker.invoke(
apiInfo.getInterfaceClassName(),
apiInfo.getMethodName(),
buildParamType(paramInfoList),
buildInvokeParam(apiRequestContext, paramInfoList)
);
}
protected void setAttachment(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
RpcContextAttachment clientAttachment = RpcContext.getClientAttachment();
clientAttachment.setAttachment(AttachmentNames.APP_ID_NAME, apiRequest.getAppId());
clientAttachment.setAttachment(AttachmentNames.API_NAME, apiRequest.getMethod());
clientAttachment.setAttachment(AttachmentNames.VERSION_NAME, apiRequest.getVersion());
clientAttachment.setAttachment(AttachmentNames.APP_AUTH_TOKEN_NAME, apiRequest.getAppAuthToken());
clientAttachment.setAttachment(AttachmentNames.NOTIFY_URL_NAME, apiRequest.getNotifyUrl());
clientAttachment.setAttachment(AttachmentNames.CLIENT_IP, apiRequestContext.getIp());
clientAttachment.setAttachment(AttachmentNames.TRACE_ID, apiRequestContext.getTraceId());
}
protected void doPreRoute(ApiRequestContext apiRequestContext, ApiInfoDTO apiInfoDTO) {
for (RouteInterceptor routeInterceptor : routeInterceptors) {
routeInterceptor.preRoute(apiRequestContext, apiInfoDTO);
}
}
protected Object doAfterRoute(ApiRequestContext apiRequestContext, ApiInfoDTO apiInfoDTO, Object result) {
Object ret = result;
for (RouteInterceptor routeInterceptor : routeInterceptors) {
ret = routeInterceptor.afterRoute(apiRequestContext, apiInfoDTO, ret);
}
return ret;
}
protected String[] buildParamType(List<ParamInfoDTO> paramInfoList) {
if (ObjectUtils.isEmpty(paramInfoList)) {
return new String[0];
}
return paramInfoList.stream()
.map(ParamInfoDTO::getType)
.toArray(String[]::new);
}
protected Object[] buildInvokeParam(ApiRequestContext apiRequestContext, List<ParamInfoDTO> paramInfoList) {
if (ObjectUtils.isEmpty(paramInfoList)) {
return new Object[0];
}
ApiRequest apiRequest = apiRequestContext.getApiRequest();
String bizContent = apiRequest.getBizContent();
JSONObject jsonObject = JSON.parseObject(bizContent);
List<Object> params = new ArrayList<>();
for (ParamInfoDTO paramInfoDTO : paramInfoList) {
String type = paramInfoDTO.getType();
String actualType = paramInfoDTO.getActualType();
// 处理文件上传
if (Objects.equals(type, FileData.class.getName()) || Objects.equals(actualType, FileData.class.getName())) {
Optional<Object> fileParam = buildFileParam(apiRequestContext, paramInfoDTO);
if (!fileParam.isPresent()) {
continue;
}
Object param = fileParam.get();
params.add(param);
} else {
if (ClassUtil.isPrimitive(type)) {
String paramName = paramInfoDTO.getName();
try {
Object value = jsonObject.getObject(paramName, ClassUtils.forName(type));
params.add(value);
jsonObject.remove(paramName);
} catch (ClassNotFoundException e) {
log.error("找不到参数class, paramInfoDTO={}, apiRequest={}", paramInfoDTO, apiRequest, e);
throw new RuntimeException("找不到class:" + type, e);
}
} else {
params.add(jsonObject);
}
}
}
return params.toArray(new Object[0]);
}
protected Optional<Object> buildFileParam(ApiRequestContext apiRequestContext, ParamInfoDTO paramInfoDTO) {
UploadContext uploadContext = apiRequestContext.getUploadContext();
if (uploadContext == null) {
return Optional.empty();
}
List<MultipartFile> files = uploadContext.getFile(paramInfoDTO.getName());
List<FileData> fileDataList = new ArrayList<>(files.size());
for (MultipartFile multipartFile : files) {
CommonFileData fileData = new CommonFileData();
fileData.setName(multipartFile.getName());
fileData.setOriginalFilename(multipartFile.getOriginalFilename());
fileData.setContentType(multipartFile.getContentType());
try {
fileData.setData(multipartFile.getBytes());
} catch (IOException e) {
log.error("读取文件内容失败, apiRequestContext={}", apiRequestContext, e);
throw new ApiException(ErrorEnum.ISP_SERVICE_UNKNOWN_ERROR, apiRequestContext.getLocale());
}
fileDataList.add(fileData);
}
String type = paramInfoDTO.getType();
Object fileParam = isCollectionClass(type) ? fileDataList : fileDataList.get(0);
return Optional.of(fileParam);
}
private boolean isCollectionClass(String className) {
Class<?> clazz = parseClass(className);
return clazz != null && Collection.class.isAssignableFrom(clazz);
}
private Class<?> parseClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
return null;
}
}
@PostConstruct
public void init() {
if (routeInterceptors == null) {
routeInterceptors = new ArrayList<>();
}
routeInterceptors.sort(Comparator.comparing(RouteInterceptor::getOrder));
}
}

View File

@@ -0,0 +1,46 @@
package com.gitee.sop.gateway.service.dubbo;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.common.StatusEnum;
import com.gitee.sop.gateway.dao.entity.ApiInfo;
import com.gitee.sop.gateway.dao.mapper.ApiInfoMapper;
import com.gitee.sop.gateway.service.manager.ApiManager;
import com.gitee.sop.gateway.util.CopyUtil;
import com.gitee.sop.support.service.ApiRegisterService;
import com.gitee.sop.support.service.dto.RegisterDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import javax.annotation.Resource;
/**
* @author 六如
*/
@Slf4j
@DubboService
public class ApiRegisterServiceImpl implements ApiRegisterService {
@Resource
private ApiManager apiCacheManager;
@Resource
private ApiInfoMapper apiInfoMapper;
@Override
public void register(RegisterDTO registerDTO) {
log.info("收到接口注册, registerDTO={}", registerDTO);
ApiInfoDTO apiInfoDTO = CopyUtil.copyBean(registerDTO, ApiInfoDTO::new);
apiInfoDTO.setStatus(StatusEnum.ENABLE.getValue());
ApiInfo apiInfo = apiInfoMapper.getByNameVersion(apiInfoDTO.getApiName(), apiInfoDTO.getApiVersion());
if (apiInfo == null) {
apiInfo = new ApiInfo();
}
CopyUtil.copyPropertiesIgnoreNull(apiInfoDTO, apiInfo);
apiInfo.setRegSource(1);
// 保存到数据库
apiInfoMapper.saveOrUpdate(apiInfo);
// 保存到缓存
apiCacheManager.save(apiInfoDTO);
}
}

View File

@@ -0,0 +1,26 @@
package com.gitee.sop.gateway.service.dubbo;
import com.gitee.sop.gateway.service.manager.IsvManager;
import com.gitee.sop.support.service.IsvService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import javax.annotation.Resource;
/**
* @author 六如
*/
@DubboService
@Slf4j
public class IsvServiceImpl implements IsvService {
@Resource
private IsvManager isvManager;
@Override
public void refresh(String appId) {
log.info("refresh Isv, appId={}", appId);
// 刷新isv信息
isvManager.reload(appId);
}
}

View File

@@ -0,0 +1,52 @@
package com.gitee.sop.gateway.service.interceptor;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.request.ApiRequestContext;
/**
* 路由拦截器
*
* @author 六如
*/
public interface RouteInterceptor {
/**
* 在路由转发前执行,签名校验通过后会立即执行此方法
*
* @param context context
* @param apiInfoDTO 接口信息
*/
default void preRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO) {
}
/**
* 微服务返回结果后执行
*
* @param context context
* @param apiInfoDTO 接口信息
* @param result 返回结果,通常是HashMap
* @return 返回格式化后的结果, 可对原结果进行修改
*/
default Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
return result;
}
/**
* 拦截器执行顺序值小优先执行建议从0开始小于0留给系统使用
*
* @return 返回顺序
*/
default int getOrder() {
return 0;
}
/**
* 是否匹配返回true执行拦截器默认true
*
* @param context context
* @return 返回true执行拦截器
*/
default boolean match(ApiRequestContext context) {
return true;
}
}

View File

@@ -0,0 +1,10 @@
package com.gitee.sop.gateway.service.interceptor;
/**
* @author 六如
*/
public class RouteInterceptorOrders {
public static final int RESULT_INTERCEPTOR = -1000;
}

View File

@@ -0,0 +1,71 @@
package com.gitee.sop.gateway.service.interceptor.internal;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptorOrders;
import com.gitee.sop.support.dto.CommonFileData;
import com.gitee.sop.support.dto.FileData;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 对结果进行处理
*
* @author 六如
*/
public class ResultRouteInterceptor implements RouteInterceptor {
private static final String CLASS = "class";
private static final String KEY_NAME = "name";
private static final String KEY_ORIGINAL_FILENAME = "originalFilename";
private static final String KEY_CONTENT_TYPE = "contentType";
private static final String KEY_BYTES = "bytes";
@Override
public Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
if (result == null) {
return new HashMap<>();
}
if (result instanceof Map) {
Map<String, Object> map = (Map<String, Object>) result;
Object className = map.get(CLASS);
// 处理文件下载
if (Objects.equals(className, FileData.class.getName()) || Objects.equals(className, CommonFileData.class.getName())) {
/*
{
"size": 27,
"bytes": "c3ByaW5nLnByb2ZpbGVzLmFjdGl2ZT1kZXYK",
"name": null,
"inputStream": {
"class": "java.io.ByteArrayInputStream"
},
"contentType": null,
"originalFilename": "application.properties",
"empty": false
*/
CommonFileData commonFileData = new CommonFileData();
commonFileData.setName(String.valueOf(map.get(KEY_NAME)));
commonFileData.setOriginalFilename(String.valueOf(map.get(KEY_ORIGINAL_FILENAME)));
commonFileData.setContentType(String.valueOf(map.get(KEY_CONTENT_TYPE)));
commonFileData.setData((byte[]) map.get(KEY_BYTES));
return commonFileData;
}
}
// 其它情况
if (result instanceof Map) {
((Map<?, ?>) result).remove(CLASS);
}
return result;
}
@Override
public int getOrder() {
return RouteInterceptorOrders.RESULT_INTERCEPTOR;
}
}

View File

@@ -0,0 +1,27 @@
package com.gitee.sop.gateway.service.manager;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import java.util.function.Supplier;
/**
* @author 六如
*/
public interface ApiManager {
void save(ApiInfoDTO apiInfoDTO);
ApiInfoDTO get(String apiName, String apiVersion);
default ApiInfoDTO getOrElse(String apiName, String apiVersion, Supplier<ApiInfoDTO> supplier) {
ApiInfoDTO apiInfoDTO = get(apiName, apiVersion);
if (apiInfoDTO == null) {
apiInfoDTO = supplier.get();
}
if (apiInfoDTO != null) {
save(apiInfoDTO);
}
return apiInfoDTO;
}
}

View File

@@ -0,0 +1,12 @@
package com.gitee.sop.gateway.service.manager;
/**
* IP黑名单管理
*
* @author 六如
*/
public interface IpBlacklistManager {
boolean contains(String ip);
}

View File

@@ -0,0 +1,12 @@
package com.gitee.sop.gateway.service.manager;
/**
* isv接口授权管理
*
* @author 六如
*/
public interface IsvApiPermissionManager {
boolean hasPermission(String appId, String apiNameVersion);
}

View File

@@ -0,0 +1,24 @@
package com.gitee.sop.gateway.service.manager;
import com.gitee.sop.gateway.service.manager.dto.IsvDTO;
/**
* @author 六如
*/
public interface IsvManager {
/**
* 获取isv信息
*
* @param appId appId
* @return 返回isv信息, 没有返回null
*/
IsvDTO getIsv(String appId);
/**
* 重新加载isv信息到内存中
*
* @param appId appId
*/
void reload(String appId);
}

View File

@@ -0,0 +1,19 @@
package com.gitee.sop.gateway.service.manager;
/**
* 秘钥管理
*
* @author 六如
*/
public interface SecretManager {
/**
* 获取用户上传的公钥
*
* @param appId appId
* @return 返回公钥内容
*/
String getIsvPublicKey(String appId);
String reload(String appId);
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.gateway.service.manager.dto;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class IsvDTO {
private String appId;
private Integer status;
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.service.manager.IpBlacklistManager;
import org.springframework.stereotype.Service;
/**
* @author 六如
*/
@Service
public class IpBlacklistManagerImpl implements IpBlacklistManager {
@Override
public boolean contains(String ip) {
return false;
}
}

View File

@@ -0,0 +1,16 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.service.manager.IsvApiPermissionManager;
import org.springframework.stereotype.Service;
/**
* @author 六如
*/
@Service
public class IsvApiPermissionManagerImpl implements IsvApiPermissionManager {
@Override
public boolean hasPermission(String appId, String apiNameVersion) {
return false;
}
}

View File

@@ -0,0 +1,39 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.dao.entity.ApiInfo;
import com.gitee.sop.gateway.dao.mapper.ApiInfoMapper;
import com.gitee.sop.gateway.service.manager.ApiManager;
import com.gitee.sop.gateway.util.CopyUtil;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 本地存储接口信息.
* @author 六如
*/
public class LocalApiManagerImpl implements ApiManager {
private static final Map<String, Optional<ApiInfoDTO>> CACHE = new ConcurrentHashMap<>();
@Resource
protected ApiInfoMapper apiInfoMapper;
@Override
public void save(ApiInfoDTO apiInfoDTO) {
String key = apiInfoDTO.buildApiNameVersion();
CACHE.put(key, Optional.of(apiInfoDTO));
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
String key = apiName + apiVersion;
return CACHE.computeIfAbsent(key, k-> {
ApiInfo apiInfo = apiInfoMapper.getByNameVersion(apiName, apiVersion);
return Optional.ofNullable(CopyUtil.copyBean(apiInfo, ApiInfoDTO::new));
}).orElse(null);
}
}

View File

@@ -0,0 +1,40 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.dao.entity.IsvInfo;
import com.gitee.sop.gateway.dao.mapper.IsvInfoMapper;
import com.gitee.sop.gateway.service.manager.IsvManager;
import com.gitee.sop.gateway.service.manager.dto.IsvDTO;
import com.gitee.sop.gateway.util.CopyUtil;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 六如
*/
public class LocalIsvManagerImpl implements IsvManager {
private static final Map<String, Optional<IsvDTO>> CACHE = new ConcurrentHashMap<>();
@Resource
protected IsvInfoMapper isvInfoMapper;
@Override
public IsvDTO getIsv(String appId) {
return CACHE.computeIfAbsent(appId, k -> {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
return Optional.ofNullable(CopyUtil.copyBean(isvInfo, IsvDTO::new));
}).orElse(null);
}
@Override
public void reload(String appId) {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
IsvDTO isvDTO = CopyUtil.copyBean(isvInfo, IsvDTO::new);
CACHE.put(appId, Optional.ofNullable(isvDTO));
}
}

View File

@@ -0,0 +1,41 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.dao.entity.IsvKeys;
import com.gitee.sop.gateway.dao.mapper.IsvKeysMapper;
import com.gitee.sop.gateway.service.manager.SecretManager;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 六如
*/
public class LocalSecretManagerImpl implements SecretManager {
private static final Map<String, Optional<String>> CACHE = new ConcurrentHashMap<>();
@Resource
protected IsvKeysMapper isvKeysMapper;
@Override
public String getIsvPublicKey(String appId) {
return CACHE.computeIfAbsent(appId, k -> {
String publicKey = isvKeysMapper.query()
.eq(IsvKeys::getAppId, appId)
.getValue(IsvKeys::getPublicKeyIsv);
return Optional.ofNullable(publicKey);
})
.orElse(null);
}
@Override
public String reload(String appId) {
String publicKey = isvKeysMapper.query()
.eq(IsvKeys::getAppId, appId)
.getValue(IsvKeys::getPublicKeyIsv);
CACHE.put(appId, Optional.ofNullable(publicKey));
return publicKey;
}
}

View File

@@ -0,0 +1,62 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.dao.entity.ApiInfo;
import com.gitee.sop.gateway.util.CopyUtil;
import com.gitee.sop.gateway.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
/**
* redis存储接口信息
*
* @author 六如
*/
@Slf4j
public class RedisApiManagerImpl extends LocalApiManagerImpl {
private static final String KEY_API = "sop:api";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void save(ApiInfoDTO apiInfoDTO) {
String key = apiInfoDTO.buildApiNameVersion();
stringRedisTemplate.opsForHash().put(KEY_API, key, JsonUtil.toJSONString(apiInfoDTO));
}
protected void cache(String key, ApiInfo apiInfo) {
ApiInfoDTO apiInfoDTO = CopyUtil.copyBean(apiInfo, ApiInfoDTO::new);
stringRedisTemplate.opsForHash().put(KEY_API, key, JsonUtil.toJSONString(apiInfoDTO));
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
String key = apiName + apiVersion;
try {
Object value = stringRedisTemplate.opsForHash().get(KEY_API, key);
if (value == null) {
return null;
}
return JsonUtil.parseObject(String.valueOf(value), ApiInfoDTO.class);
} catch (Exception e) {
log.error("redis访问失败", e);
return super.get(apiName, apiVersion);
}
}
@PostConstruct
public void init() {
log.info("load apiInfo to redis");
List<ApiInfo> apiInfos = this.apiInfoMapper.listAll();
for (ApiInfo apiInfo : apiInfos) {
String key = apiInfo.getApiName() + apiInfo.getApiVersion();
this.cache(key, apiInfo);
}
}
}

View File

@@ -0,0 +1,61 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.dao.entity.IsvInfo;
import com.gitee.sop.gateway.service.manager.dto.IsvDTO;
import com.gitee.sop.gateway.util.CopyUtil;
import com.gitee.sop.gateway.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
/**
* @author 六如
*/
@Slf4j
public class RedisIsvManagerImpl extends LocalIsvManagerImpl {
private static final String KEY_ISV = "sop:isv";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public IsvDTO getIsv(String appId) {
try {
Object value = stringRedisTemplate.opsForHash().get(KEY_ISV, appId);
if (value == null) {
IsvInfo isvInfo = this.isvInfoMapper.getByAppId(appId);
return this.cache(isvInfo);
}
return JsonUtil.parseObject(String.valueOf(value), IsvDTO.class);
} catch (Exception e) {
log.error("操作redis失败", e);
return super.getIsv(appId);
}
}
@Override
public void reload(String appId) {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
this.cache(isvInfo);
}
@PostConstruct
public void init() {
log.info("load isvInfo to redis");
List<IsvInfo> isvInfos = this.isvInfoMapper.listAll();
for (IsvInfo isvInfo : isvInfos) {
this.cache(isvInfo);
}
}
protected IsvDTO cache(IsvInfo isvInfo) {
IsvDTO isvDTO = CopyUtil.copyBean(isvInfo, IsvDTO::new);
stringRedisTemplate.opsForHash().put(KEY_ISV, isvInfo.getAppId(), JsonUtil.toJSONString(isvDTO));
return isvDTO;
}
}

View File

@@ -0,0 +1,59 @@
package com.gitee.sop.gateway.service.manager.impl;
import com.gitee.sop.gateway.dao.entity.IsvKeys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
/**
* @author 六如
*/
@Slf4j
public class RedisSecretManager extends LocalSecretManagerImpl {
private static final String KEY_ISV = "sop:sec";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public String getIsvPublicKey(String appId) {
try {
Object value = stringRedisTemplate.opsForHash().get(KEY_ISV, appId);
if (value == null) {
return this.reload(appId);
}
return String.valueOf(value);
} catch (Exception e) {
log.error("操作redis失败", e);
return super.getIsvPublicKey(appId);
}
}
@Override
public String reload(String appId) {
IsvKeys isvKeys = this.isvKeysMapper.getByAppId(appId);
return this.cache(appId, isvKeys);
}
protected String cache(String appId, IsvKeys isvKeys) {
String publicKey = Optional.ofNullable(isvKeys).map(IsvKeys::getPublicKeyIsv).orElse("");
stringRedisTemplate.opsForHash().put(KEY_ISV, appId, publicKey);
return publicKey;
}
@PostConstruct
public void init() {
log.info("load isvKey to redis");
List<IsvKeys> isvKeys = this.isvKeysMapper.listAll();
for (IsvKeys isvKey : isvKeys) {
this.cache(isvKey.getAppId(), isvKey);
}
}
}

View File

@@ -0,0 +1,47 @@
package com.gitee.sop.gateway.service.validate;
import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.message.ErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.utils.StringUtils;
/**
* @author 六如
*/
@Slf4j
public abstract class AbstractSigner implements Signer {
/**
* 构建服务端签名串
*
* @param apiRequestContext 接口参数
* @param secret 秘钥
* @return 返回服务端签名串
*/
protected abstract String buildServerSign(ApiRequestContext apiRequestContext, String secret);
@Override
public boolean checkSign(ApiRequestContext apiRequestContext, String publicKey) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
String clientSign = apiRequest.getSign();
if (StringUtils.isBlank(clientSign)) {
throw new ApiException(ErrorEnum.ISV_MISSING_SIGNATURE, apiRequestContext.getLocale());
}
String serverSign = buildServerSign(apiRequestContext, publicKey);
return clientSign.equals(serverSign);
}
protected static String byte2hex(byte[] bytes) {
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex);
}
return sign.toString();
}
}

View File

@@ -0,0 +1,60 @@
package com.gitee.sop.gateway.service.validate;
import com.gitee.sop.gateway.util.AESUtil;
import com.gitee.sop.gateway.util.RSANewUtil;
import com.gitee.sop.gateway.util.RSAUtil;
import org.apache.commons.codec.digest.DigestUtils;
/**
* 负责各类加解密
*
* @author 六如
*/
public class ApiEncrypter implements Encrypter {
@Override
public String aesEncryptToHex(String content, String password) throws Exception {
return AESUtil.encryptToHex(content, password);
}
@Override
public String aesDecryptFromHex(String hex, String password) throws Exception {
return AESUtil.decryptFromHex(hex, password);
}
@Override
public String aesEncryptToBase64String(String content, String password) throws Exception {
return AESUtil.encryptToBase64String(content, password);
}
@Override
public String aesDecryptFromBase64String(String base64String, String password) throws Exception {
return AESUtil.decryptFromBase64String(base64String, password);
}
@Override
public String rsaDecryptByPrivateKey(String data, String privateKey) throws Exception {
return RSAUtil.decryptByPrivateKey(data, privateKey);
}
@Override
public String rsaEncryptByPrivateKey(String data, String privateKey) throws Exception {
return RSAUtil.encryptByPrivateKey(data, privateKey);
}
@Override
public String rsaDecryptByPrivateKeyNew(String data, String privateKey) throws Exception {
return RSANewUtil.decryptByPrivateKey(data, privateKey);
}
@Override
public String rsaEncryptByPrivateKeyNew(String data, String privateKey) throws Exception {
return RSANewUtil.encryptByPrivateKey(data, privateKey);
}
@Override
public String md5(String value) {
return DigestUtils.md5Hex(value);
}
}

View File

@@ -0,0 +1,298 @@
package com.gitee.sop.gateway.service.validate;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.common.StatusEnum;
import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.request.RequestFormatEnum;
import com.gitee.sop.gateway.request.UploadContext;
import com.gitee.sop.gateway.service.manager.ApiManager;
import com.gitee.sop.gateway.service.manager.IpBlacklistManager;
import com.gitee.sop.gateway.service.manager.IsvApiPermissionManager;
import com.gitee.sop.gateway.service.manager.IsvManager;
import com.gitee.sop.gateway.service.manager.SecretManager;
import com.gitee.sop.gateway.service.manager.dto.IsvDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Locale;
/**
* 负责校验,校验工作都在这里
*
* @author 六如
*/
@Slf4j
@Service
public class ApiValidator implements Validator {
private static final long MILLISECOND_OF_ONE_SECOND = 1000;
/**
* 单个文件内容最大值
*/
@Value("${upload.one-file-max-size}")
private DataSize oneFileMaxSize;
/**
* 总文件最大值
*/
@Value("${upload.total-file-max-size}")
private DataSize maxFileSize;
@Resource
private Signer signer;
@Resource
private ApiConfig apiConfig;
@Resource
private ApiManager apiCacheManager;
@Resource
private IpBlacklistManager ipBlacklistManager;
@Resource
private IsvApiPermissionManager isvApiPermissionManager;
@Resource
private IsvManager isvManager;
@Resource
private SecretManager secretManager;
private DateTimeFormatter dateTimeFormatter;
@Override
public ApiInfoDTO validate(ApiRequestContext apiRequestContext) {
// 校验字段完整性
checkField(apiRequestContext);
// 检查isv
IsvDTO isvDTO = checkIsv(apiRequestContext);
// 校验签名
checkSign(apiRequestContext, isvDTO);
// 检查是否超时
checkTimeout(apiRequestContext);
// 检查格式化
checkFormat(apiRequestContext);
// IP能否访问
checkIP(apiRequestContext);
ApiRequest apiRequest = apiRequestContext.getApiRequest();
ApiInfoDTO apiInfo = apiCacheManager.get(apiRequest.getMethod(), apiRequest.getVersion());
// 检查接口信息
checkApiInfo(apiRequestContext, apiInfo);
// 检查上传文件
checkUploadFile(apiRequestContext);
// 检查token
checkToken(apiRequestContext);
return apiInfo;
}
public void checkApiInfo(ApiRequestContext apiRequestContext, ApiInfoDTO apiInfoDTO) {
// 检查路由是否存在
if (apiInfoDTO == null) {
throw new ApiException(ErrorEnum.ISV_INVALID_METHOD, apiRequestContext.getLocale());
}
// 检查路由是否启用
if (!BooleanUtils.toBoolean(apiInfoDTO.getStatus())) {
throw new ApiException(ErrorEnum.ISP_API_DISABLED, apiRequestContext.getLocale());
}
// 校验是否需要授权访问
boolean needCheckPermission = BooleanUtils.toBoolean(apiInfoDTO.getIsPermission());
if (needCheckPermission) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
String appKey = apiRequest.getAppId();
String nameVersion = apiRequest.takeNameVersion();
boolean hasPermission = isvApiPermissionManager.hasPermission(appKey, nameVersion);
if (!hasPermission) {
throw new ApiException(ErrorEnum.ISV_ROUTE_NO_PERMISSIONS, apiRequestContext.getLocale());
}
}
}
public void checkField(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
if (apiRequest == null) {
throw new ApiException(ErrorEnum.ISV_INVALID_PARAMETER, apiRequestContext.getLocale());
}
Locale locale = apiRequestContext.getLocale();
if (ObjectUtils.isEmpty(apiRequest.getAppId())) {
throw new ApiException(ErrorEnum.ISV_MISSING_APP_ID, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getMethod())) {
throw new ApiException(ErrorEnum.ISV_MISSING_METHOD, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getVersion())) {
throw new ApiException(ErrorEnum.ISV_MISSING_VERSION, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getSignType())) {
throw new ApiException(ErrorEnum.ISV_MISSING_SIGNATURE_CONFIG, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getCharset())) {
throw new ApiException(ErrorEnum.ISV_INVALID_CHARSET, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getSign())) {
throw new ApiException(ErrorEnum.ISV_MISSING_SIGNATURE, locale);
}
if (ObjectUtils.isEmpty(apiRequest.getTimestamp())) {
throw new ApiException(ErrorEnum.ISV_MISSING_TIMESTAMP, locale);
}
}
/**
* 是否在IP黑名单中
*
* @param apiRequestContext 接口参数
*/
protected void checkIP(ApiRequestContext apiRequestContext) {
String ip = apiRequestContext.getIp();
if (ipBlacklistManager.contains(ip)) {
throw new ApiException(ErrorEnum.ISV_IP_FORBIDDEN, apiRequestContext.getLocale());
}
}
/**
* 校验上传文件内容
*
* @param apiRequestContext apiRequestContext
*/
protected void checkUploadFile(ApiRequestContext apiRequestContext) {
// 校验上传文件内容
UploadContext uploadContext = apiRequestContext.getUploadContext();
if (uploadContext != null) {
List<MultipartFile> allFiles = uploadContext.getAllFile();
if (ObjectUtils.isEmpty(allFiles)) {
return;
}
for (MultipartFile multipartFile : allFiles) {
checkSingleFileSize(apiRequestContext, multipartFile);
}
long totalSize = allFiles.stream()
.map(MultipartFile::getSize)
.mapToLong(Long::longValue)
.sum();
if (totalSize > maxFileSize.toBytes()) {
throw new ApiException(ErrorEnum.ISV_INVALID_FILE_SIZE, apiRequestContext.getLocale(), totalSize, maxFileSize);
}
}
}
/**
* 校验单个文件大小
*
* @param file 文件
*/
private void checkSingleFileSize(ApiRequestContext apiRequestContext, MultipartFile file) {
long fileSize = file.getSize();
long maxSize = oneFileMaxSize.toBytes();
if (fileSize > maxSize) {
throw new ApiException(ErrorEnum.ISV_INVALID_FILE_SIZE, apiRequestContext.getLocale(), fileSize, maxSize);
}
}
protected void checkTimeout(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
int timeoutSeconds = apiConfig.getTimeoutSeconds();
// 如果设置为0表示不校验
if (timeoutSeconds == 0) {
return;
}
if (timeoutSeconds < 0) {
throw new IllegalArgumentException("服务端timeoutSeconds设置错误");
}
String requestTime = apiRequest.getTimestamp();
try {
LocalDateTime localDateTime = LocalDateTime.parse(requestTime, dateTimeFormatter);
long diffMills = Duration.between(localDateTime, LocalDateTime.now()).toMillis();
if (diffMills > timeoutSeconds * MILLISECOND_OF_ONE_SECOND) {
throw new ApiException(ErrorEnum.ISV_INVALID_TIMESTAMP, apiRequestContext.getLocale());
}
} catch (DateTimeParseException e) {
throw new ApiException(ErrorEnum.ISV_INVALID_TIMESTAMP, apiRequestContext.getLocale(), apiRequest.takeNameVersion());
}
}
protected IsvDTO checkIsv(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
IsvDTO isv = isvManager.getIsv(apiRequest.getAppId());
// 没有用户
if (isv == null) {
throw new ApiException(ErrorEnum.ISV_INVALID_APP_ID, apiRequestContext.getLocale());
}
// 禁止访问
if (isv.getStatus() == null || isv.getStatus() == StatusEnum.DISABLE.getValue()) {
throw new ApiException(ErrorEnum.ISV_ACCESS_FORBIDDEN, apiRequestContext.getLocale());
}
return isv;
}
protected void checkSign(ApiRequestContext apiRequestContext, IsvDTO isv) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
String clientSign = apiRequest.getSign();
if (ObjectUtils.isEmpty(clientSign)) {
throw new ApiException(ErrorEnum.ISV_MISSING_SIGNATURE, apiRequestContext.getLocale(),
apiRequest.takeNameVersion(), apiConfig.getSignName());
}
// ISV上传的公钥
String publicKey = secretManager.getIsvPublicKey(isv.getAppId());
if (ObjectUtils.isEmpty(publicKey)) {
throw new ApiException(ErrorEnum.ISV_MISSING_SIGNATURE_CONFIG, apiRequestContext.getLocale(),
apiRequest.takeNameVersion());
}
// 错误的sign
if (!signer.checkSign(apiRequestContext, publicKey)) {
throw new ApiException(ErrorEnum.ISV_INVALID_SIGNATURE, apiRequestContext.getLocale(),
apiRequest.takeNameVersion());
}
}
protected void checkFormat(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest();
String format = apiRequest.getFormat();
if (ObjectUtils.isEmpty(format)) {
return;
}
if (RequestFormatEnum.of(format) == RequestFormatEnum.NONE) {
throw new ApiException(ErrorEnum.ISV_INVALID_FORMAT, apiRequestContext.getLocale(),
apiRequest.takeNameVersion(), format);
}
}
/**
* 校验token
*
* @param apiRequestContext 参数
*/
protected void checkToken(ApiRequestContext apiRequestContext) {
// todo:校验token
}
@PostConstruct
public void init() {
dateTimeFormatter = DateTimeFormatter.ofPattern(apiConfig.getTimestampPattern());
}
}

Some files were not shown because too many files have changed in this diff Show More