背景
互聯網公司隨着業務的發展,系統間或多或少會開放一些對外接口,這些接口都會以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
正常情況
異常情況