Java实现系统统一对外开放网关入口设计

时间:2024-02-22 22:35:16

背景

互联网公司随着业务的发展,系统间或多或少会开放一些对外接口,这些接口都会以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

正常情况
在这里插入图片描述
异常情况在这里插入图片描述
在这里插入图片描述

源码地址
https://github.com/mengq0815/open-api-project