Java實現系統統一對外開放網關入口設計


背景

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM