背景
互联网公司随着业务的发展,系统间或多或少会开放一些对外接口,这些接口都会以API的形式提供给外部。为了方便统一管理,统一鉴权,统一签名认证机制,流量预警等引入了统一网关。API网关是一是对外接口唯一入口。
开放接口的安全性
对外开放的接口,如何保证安全通信,防止数据被恶意篡改等攻击呢?怎么证明是你发的请 求呢?
比较流行的方式一搬是
- 加密
- 加签
注:加密是密文传输,接收方需要解密。加签是明文加签名传输,接收方验签防止数据篡改
Java版开放接口设计
本文用到的主要技术点
1.java泛型
2.rsa加签验签
3.springBoot
4.hibernate-validator注解式参数校验
统一网关接口介绍
公共参数
参数 | 类型 | 是否必填 | 最大长度 | 描述 |
---|---|---|---|---|
app_id | String | 是 | 32 | 业务方appId |
method | String | 是 | 128 | 请求方法 |
version | String | 是 | 10 | 默认:1.0 |
api_request_id | String | 是 | 32 | 随机请求标识,用于区分每一次请求 |
charset | String | 是 | 16 | 默认:UTF-8 |
sign_type | String | 是 | 10 | 签名类型:RSA或RSA2 |
sign | String | 是 | - | 签名 |
content | String | 是 | - | 业务内容 :json 格式字符串 |
返回内容
参数 | 类型 | 是否必填 | 最大长度 | 描述 |
---|---|---|---|---|
success | boolean | 是 | 16 | 是否成功 |
data | Object | 是 | - | 返回业务信息(具体见业务接口) |
error_code | String | 是 | 10 | 错误码(success为false时必填) |
error_msg | String | 是 | 128 | 错误信息码(success为false时必填) |
签名规则
① 签名参数剔除sign_type 、 sign
② 将剩余参数第一个字符按照ASCII码排序(字母升序排序),遇到相同字母则按第二个字符ASCII码排序,以此类推
③ 将排序后的参数按照组合“参数=参数值”的格式拼接,并用&字符连接起来,生成的字符串为待签名字符串
④ 使用RSA算法通过私钥生成签名
RSA === SHA1 --> base64
RSA2 === SHA256 --> base64
代码实践
注:源码见文章末
maven依赖
open-api-project > pom
<?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"
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>
<groupId>com.open.api</groupId>
<artifactId>open-api-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.14.RELEASE</version>
<relativePath/>
</parent>
<modules>
<module>open-api-common</module>
<module>open-api-web</module>
</modules>
<!-- 统一 jar 版本号 -->
<properties>
<!-- 统一子项目 版本号 -->
<project.version>1.0.0.20190312</project.version>
<!-- jar包编码 -->
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>1.5.14.RELEASE</spring.boot.version>
<spring.version>4.3.17.RELEASE</spring.version>
<redisson.version>3.5.4</redisson.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-collections.version>3.2.2</commons-collections.version>
<commons-io.version>2.5</commons-io.version>
<commons-net.version>3.2</commons-net.version>
<commons-codec.version>1.10</commons-codec.version>
<commons-compress.version>1.12</commons-compress.version>
<httpclient.version>4.5.2</httpclient.version>
<fastjson.version>1.2.39</fastjson.version>
<alibaba.common.lang.version>1.0</alibaba.common.lang.version>
<log4j2.version>2.8.1</log4j2.version>
<disruptor.version>3.3.6</disruptor.version>
<slf4j.version>1.7.25</slf4j.version>
<junit.version>4.12</junit.version>
<guava.version>21.0</guava.version>
<javax.servlet.version>3.1.0</javax.servlet.version>
<hutool.version>3.0.9</hutool.version>
<lombok.version>1.16.4</lombok.version>
<hibernate-validator.version>5.4.1.Final</hibernate-validator.version>
<jcraft.version>0.1.54</jcraft.version>
</properties>
<dependencies>
</dependencies>
<!-- 依赖声 >>> 子 module 中需要的时候自己引入,无需要带版本号 -->
<dependencyManagement>
<dependencies>
<!-- 配置gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring核心包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- apache 相关 开始 -->
<!-- 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-collections.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--fastjson json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.common.lang</groupId>
<artifactId>toolkit-common-lang</artifactId>
<version>${alibaba.common.lang.version}</version>
</dependency>
<!-- ali 相关结束 -->
<!-- 日志文件管理包 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-ext</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j2.version}</version>
</dependency>
<!-- disruptor 用于log4j2的异步日志 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- javax.servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- 校验工具 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- lombok version -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.7.4.ALL</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
open-api-web > pom
<?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"
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>
<artifactId>open-api-parent</artifactId>
<groupId>com.open.api</groupId>
<version>1.0.0</version>
</parent>
<groupId>com.open.api</groupId>
<artifactId>open-api-web</artifactId>
<version>${project.version}</version>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.open.api</groupId>
<artifactId>open-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- spring boot starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<failOnError>true</failOnError>
<verbose>true</verbose>
<fork>true</fork>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
添加配置文件
server.port=8821
#日志配置
logging.level.root=WARN
logging.level.net.sf=WARN
logging.level.com.open.api=debug
#是否校验签名
open.api.common.key.isCheckSign=false
#开放接口公钥
open.api.common.key.publicKey=MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGxwWNSdZ1T4y2sMhESQJdTKDhhtTtVQT8eWxXiT2/TJDE2vZwqjcTFZZmK6ppibN9JL3VK5bU2Bc81v15XwlgZIgjrV9hRQNH1awIz4YkGOjEZKryHAXNYimM1z8Y4cOJiBafpVLmIDhz1DniAiyG+5cTVw2P4tIfE0L3ty+YCnAgMBAAE=
#开放接口私钥
open.api.common.key.privateKey=MIICWgIBAAKBgGxwWNSdZ1T4y2sMhESQJdTKDhhtTtVQT8eWxXiT2/TJDE2vZwqjcTFZZmK6ppibN9JL3VK5bU2Bc81v15XwlgZIgjrV9hRQNH1awIz4YkGOjEZKryHAXNYimM1z8Y4cOJiBafpVLmIDhz1DniAiyG+5cTVw2P4tIfE0L3ty+YCnAgMBAAECgYA1jodQ+yy92uMcy+HHuyn0Hpc3mUUGNdQxT1XYZ66LB4D8HVVW+8I8DVt0B5ugY4j+ZFm7Mbm6PeVj4YkolNqDOnDSxEGyVMEfjTJ3ipcJjVPbdEOLCjspgCnedrfbx/hDVURCmu4WFzbcMGwn9KjIxaE93Xolo57tbE1vYAWkAQJBALkBcKCmmoTiFhlR7QopagFppEAyo5kS/dOMpLDJQlWnFeJC93ISap0fNc7AXMsYVmCIebyFEtjWKWgwv05AzEcCQQCWDSZrT0wPgI7gnARNxklHzyuoS6brIXKakWvz9CPJ8//LQaZjrFiLYazK+itbGUcrRhh4ydWUzDcRQXVMarihAkAsjSI4LaasNV2o/0eb2NlEOdJp+0fWRvKFDStjvzOQOMpWUFYSTEkMSUXF4iD2b4ftezAFq+4b9YbHJmYLTCNlAkBlpvb2D7xpbCBfDZLk1YXjffgHhWjJNdmb2RSXKjfsor4RhqIgOCusETmsMJqalp9eM5h0i9eDfG155Sx/3nTBAkBmaJfxnoXg/bQPDoNIxbp/jWbQ1WThvygeD2aKjh6BtmzlkmBI0/8Qh2lGr4QoKNL4LVIf6afNeSyxmQeo35cT
启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
统一异常处理
import com.open.api.enums.ApiExceptionEnum;
import com.open.api.exception.BusinessException;
import com.open.api.model.ResultModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import java.text.MessageFormat;
/**
* 统一异常处理
*/
@ControllerAdvice
@EnableAspectJAutoProxy
public class ExceptionAdvice {
/**
* 日志
*/
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@CrossOrigin
@ResponseBody
@ExceptionHandler(value = Exception.class)
public ResultModel defaultExceptionHandler(Exception exception, HttpServletResponse response) {
ResultModel result;
try {
logger.warn("全局业务处理异常 >> error = {}", exception.getMessage(), exception);
throw exception;
} catch (BusinessException e) {
result = ResultModel.error(e.getCode(), e.getMsg());
} catch (HttpRequestMethodNotSupportedException e) {
String errorMsg = MessageFormat.format(ApiExceptionEnum.INVALID_REQUEST_ERROR.getMsg(), e.getMethod(), e.getSupportedHttpMethods());
result = ResultModel.error(ApiExceptionEnum.INVALID_REQUEST_ERROR.getCode(), errorMsg);
} catch (MissingServletRequestParameterException e) {
String errorMsg = MessageFormat.format(ApiExceptionEnum.INVALID_PUBLIC_PARAM.getMsg(), e.getMessage());
result = ResultModel.error(ApiExceptionEnum.INVALID_PUBLIC_PARAM.getCode(), errorMsg);
} catch (Exception e) {
result = ResultModel.error(ApiExceptionEnum.SYSTEM_ERROR.getCode(), ApiExceptionEnum.SYSTEM_ERROR.getMsg());
}
return result;
}
}
自定义注解
开放接口方法注解
package com.open.api.annotation;
import java.lang.annotation.*;
/**
* 开放接口注解
*/
@Documented
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApi {
/**
* api 方法名
*
* @return
*/
String method();
/**
* 方法描述
*/
String desc() default "";
}
开放接口实现类注解
package com.open.api.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
* 开放接口实现类注解
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface OpenApiService {
}
API接口初始化容器与扫描器
/**
* Api 初始化容器
*/
@Service
public class ApiContainer extends HashMap<String, ApiModel> {
}
/**
* api接口对象
*/
public class ApiModel {
/**
* 类 spring bean
*/
private String beanName;
/**
* 方法对象
*/
private Method method;
/**
* 业务参数
*/
private String paramName;
public ApiModel(String beanName, Method method, String paramName) {
this.beanName = beanName;
this.method = method;
this.paramName = paramName;
}
//省略 get/set
}
package com.open.api.support;
import com.open.api.annotation.OpenApi;
import com.open.api.annotation.OpenApiService;
import com.open.api.config.context.ApplicationContextHelper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Api接口扫描器
*/
@Component
public class ApiScanner implements CommandLineRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiScanner.class);
/**
* 方法签名拆分正则
*/
private static final Pattern PATTERN = Pattern.compile("\\s+(.*)\\s+((.*)\\.(.*))\\((.*)\\)", Pattern.DOTALL);
/**
* 参数分隔符
*/
private static final String PARAMS_SEPARATOR = ",";
/**
* 统计扫描次数
*/
private AtomicInteger atomicInteger = new AtomicInteger(0);
@Resource
private ApiContainer apiContainer;
@Override
public void run(String... var1) throws Exception {
//扫描所有使用@OpenApiService注解的类
Map<String, Object> openApiServiceBeanMap = ApplicationContextHelper.getBeansWithAnnotation(OpenApiService.class);
if (null == openApiServiceBeanMap || openApiServiceBeanMap.isEmpty()) {
LOGGER.info("open api service bean map is empty");
return;
}
for (Map.Entry<String, Object> map : openApiServiceBeanMap.entrySet()) {
//获取扫描类下所有方法
Method[] methods = ReflectionUtils.getAllDeclaredMethods(map.getValue().getClass());
for (Method method : methods) {
atomicInteger.incrementAndGet();
//找到带有OpenApi 注解的方法
OpenApi openApi = AnnotationUtils.findAnnotation(method, OpenApi.class);
if (null == openApi) {
continue;
}
//获取业务参数对象
String paramName = getParamName(method);
if (StringUtils.isBlank(paramName)) {
LOGGER.warn("Api接口业务参数缺失 >> method = {}", openApi.method());
continue;
}
//组建ApiModel- 放入api容器
apiContainer.put(openApi.method(), new ApiModel(map.getKey(), method, paramName));
LOGGER.info("Api接口加载成功 >> method = {} , desc={}", openApi.method(), openApi.desc());
}
}
LOGGER.info("Api接口容器加载完毕 >> size = {} loopTimes={}", apiContainer.size(), atomicInteger.get());
}
/**
* 获取业务参数对象
*
* @param method
* @return
*/
private String getParamName(Method method) {
ArrayList<String> result = new ArrayList<>();
final Matcher matcher = PATTERN.matcher(method.toGenericString());
if (matcher.find()) {
int groupCount = matcher.groupCount() + 1;
for (int i = 0; i < groupCount; i++) {
result.add(matcher.group(i));
}
}
//获取参数部分
if (result.size() >= 6) {
String[] params =
StringUtils.splitByWholeSeparatorPreserveAllTokens(result.get(5), PARAMS_SEPARATOR);
if (params.length >= 2) {
return params[1];
}
}
return null;
}
}
API请求处理客户端
package com.open.api.client;
import com.alibaba.fastjson.JSON;
import com.alipay.api.internal.util.AlipaySignature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.open.api.config.context.ApplicationContextHelper;
import com.open.api.config.property.ApplicationProperty;
import com.open.api.enums.ApiExceptionEnum;
import com.open.api.exception.BusinessException;
import com.open.api.support.ApiContainer;
import com.open.api.support.ApiModel;
import com.open.api.model.ResultModel;
import com.open.api.util.ValidateUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* Api请求客户端
*
* @author 码农猿
*/
@Service
public class ApiClient {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ApiClient.class);
/**
* jackson 序列化工具类
*/
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
/**
* Api本地容器
*/
private final ApiContainer apiContainer;
public ApiClient(ApiContainer apiContainer) {
this.apiContainer = apiContainer;
}
@Resource
private ApplicationProperty applicationProperty;
/**
* 验签
*
* @param params 请求参数
* @param requestRandomId 请求随机标识(用于日志中分辨是否是同一次请求)
* @param charset 请求编码
* @param signType 签名格式
* @author 码农猿
*/
public void checkSign(Map<String, Object> params, String requestRandomId, String charset, String signType) {
try {
//校验签名开关
if (!applicationProperty.getIsCheckSign()) {
LOGGER.warn("【{}】>> 验签开关关闭", requestRandomId);
return;
}
//map类型转换
Map<String, String> map = new HashMap<>(params.size());
for (String s : params.keySet()) {
map.put(s, params.get(s).toString());
}
LOGGER.warn("【{}】 >> 验签参数 {}", requestRandomId, map);
boolean checkSign = AlipaySignature.rsaCheckV1(map, applicationProperty.getPublicKey(), charset, signType);
if (!checkSign) {
LOGGER.info("【{}】 >> 验签失败 >> params = {}", requestRandomId, JSON.toJSONString(params));
throw new BusinessException(ApiExceptionEnum.INVALID_SIGN);
}
LOGGER.warn("【{}】 >> 验签成功", requestRandomId);
} catch (Exception e) {
LOGGER.error("【{}】 >> 验签异常 >> params = {}, error = {}",
requestRandomId, JSON.toJSONString(params), ExceptionUtils.getStackTrace(e));
throw new BusinessException(ApiExceptionEnum.INVALID_SIGN);
}
}
/**
* Api调用方法
*
* @param method 请求方法
* @param requestRandomId 请求随机标识
* @param content 请求体
* @author 码农猿
*/
public ResultModel invoke(String method, String requestRandomId, String content) throws Throwable {
//获取api方法
ApiModel apiModel = apiContainer.get(method);
if (null == apiModel) {
LOGGER.info("【{}】 >> API方法不存在 >> method = {}", requestRandomId, method);
throw new BusinessException(ApiExceptionEnum.API_NOT_EXIST);
}
//获得spring bean
Object bean = ApplicationContextHelper.getBean(apiModel.getBeanName());
if (null == bean) {
LOGGER.warn("【{}】 >> API方法不存在 >> method = {}, beanName = {}", requestRandomId, method, apiModel.getBeanName());
throw new BusinessException(ApiExceptionEnum.API_NOT_EXIST);
}
//处理业务参数
// 忽略JSON字符串中存在,而在Java中不存在的属性
JSON_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 设置下划线序列化方式
JSON_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
Object result = JSON_MAPPER.readValue(content, Class.forName(apiModel.getParamName()));
//校验参数
ValidateUtils.validate(result);
//执行对应方法
try {
Object obj = apiModel.getMethod().invoke(bean, requestRandomId, result);
return ResultModel.success(obj);
} catch (Exception e) {
if (e instanceof InvocationTargetException) {
throw ((InvocationTargetException) e).getTargetException();
}
throw new BusinessException(ApiExceptionEnum.SYSTEM_ERROR);
}
}
}
创建测试接口
入参BO
/**
* 使用注解做参数校验
*/
public class Test1BO implements Serializable {
private static final long serialVersionUID = -1L;
@Valid
@NotEmpty(message = "集合不为空!")
@Size(min = 1, message = "最小为{min}")
private List<Item> itemList;
//省略 get/set
/**
* 内部类
*/
public static class Item {
@NotBlank(message = "username 不能为空!")
private String username;
@NotBlank(message = "password 不能为空!")
private String password;
@NotBlank(message = "realName 不能为空!")
private String realName;
//省略 get/set
}
}
测试service接口
注意:注解@OpenApi 使用 ,method就是入参中的方法
/**
* 测试开放接口1
*/
public interface TestOneService {
/**
* 方法1
*/
@OpenApi(method = "open.api.test.one.method1", desc = "测试接口1,方法1")
void testMethod1(String requestId, Test1BO test1BO);
}
测试接口实现类
/**
* 测试开放接口1
* <p>
* 注解@OpenApiService > 开放接口自定义注解,用于启动时扫描接口
*/
@Service
public class TestOneServiceImpl implements TestOneService {
/**
* 日志
*/
private static final Logger LOGGER = LoggerFactory.getLogger(TestOneServiceImpl.class);
/**
* 方法1
*/
@Override
public void testMethod1(String requestId, Test1BO test1BO) {
LOGGER.info("【{}】>> 测试开放接口1 >> 方法1 params={}", requestId, JSON.toJSONString(test1BO));
}
}
统一网关开放接口controller
package com.open.api.controller;
import com.alibaba.fastjson.JSON;
import com.open.api.client.ApiClient;
import com.open.api.model.ResultModel;
import com.open.api.util.SystemClock;
import jodd.util.StringPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* 统一网关
*/
@RestController
@RequestMapping("/open")
public class OpenApiController {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiController.class);
@Autowired
private ApiClient apiClient;
/**
* 统一网关入口
*
* @param method 请求方法
* @param version 版本
* @param apiRequestId 请求标识(用于日志中分辨是否是同一次请求)
* @param charset 请求编码
* @param signType 签名格式
* @param sign 签名
* @param content 业务内容参数
* @author 码农猿
*/
@PostMapping("/gateway")
public ResultModel gateway(@RequestParam(value = "app_id", required = true) String appId,
@RequestParam(value = "method", required = true) String method,
@RequestParam(value = "version", required = true) String version,
@RequestParam(value = "api_request_id", required = true) String apiRequestId,
@RequestParam(value = "charset", required = true) String charset,
@RequestParam(value = "sign_type", required = true) String signType,
@RequestParam(value = "sign", required = true) String sign,
@RequestParam(value = "content", required = true) String content,
HttpServletRequest request) throws Throwable {
Map<String, Object> params = WebUtils.getParametersStartingWith(request, StringPool.EMPTY);
LOGGER.info("【{}】>> 网关执行开始 >> method={} params = {}", apiRequestId, method, JSON.toJSONString(params));
long start = SystemClock.millisClock().now();
//验签
apiClient.checkSign(params, apiRequestId, charset, signType);
//请求接口
ResultModel result = apiClient.invoke(method, apiRequestId, content);
LOGGER.info("【{}】>> 网关执行结束 >> method={},result = {}, times = {} ms",
apiRequestId, method, JSON.toJSONString(result), (SystemClock.millisClock().now() - start));
return result;
}
}
接口测试
注:为了方便调试先将配置文件中的验签开关修改为 false
正常情况
异常情况