博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java实现系统统一对外开放网关入口设计
阅读量:5341 次
发布时间:2019-06-15

本文共 30053 字,大约阅读时间需要 100 分钟。

背景

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

4.0.0
com.open.api
open-api-parent
1.0.0
pom
org.springframework.boot
spring-boot-starter-parent
1.5.14.RELEASE
open-api-common
open-api-web
1.0.0.20190312
UTF-8
UTF-8
1.5.14.RELEASE
4.3.17.RELEASE
3.5.4
3.4
3.2.2
2.5
3.2
1.10
1.12
4.5.2
1.2.39
1.0
2.8.1
3.3.6
1.7.25
4.12
21.0
3.1.0
3.0.9
1.16.4
5.4.1.Final
0.1.54
com.google.code.gson
gson
2.2.4
org.springframework.boot
spring-boot-dependencies
${spring.boot.version}
pom
import
org.springframework
spring-core
${spring.version}
org.springframework
spring-web
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-oxm
${spring.version}
org.springframework
spring-tx
${spring.version}
org.springframework
spring-jdbc
${spring.version}
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-aop
${spring.version}
org.springframework
spring-context-support
${spring.version}
org.springframework
spring-test
${spring.version}
test
org.redisson
redisson
${redisson.version}
org.apache.commons
commons-lang3
${commons-lang3.version}
commons-collections
commons-collections
${commons-collections.version}
commons-io
commons-io
${commons-io.version}
commons-net
commons-net
${commons-net.version}
commons-codec
commons-codec
${commons-codec.version}
org.apache.commons
commons-compress
${commons-compress.version}
org.apache.httpcomponents
httpclient
${httpclient.version}
commons-logging
commons-logging
com.alibaba
fastjson
${fastjson.version}
com.alibaba.common.lang
toolkit-common-lang
${alibaba.common.lang.version}
org.slf4j
slf4j-api
${slf4j.version}
org.slf4j
slf4j-ext
${slf4j.version}
org.apache.logging.log4j
log4j-api
${log4j2.version}
org.apache.logging.log4j
log4j-core
${log4j2.version}
org.apache.logging.log4j
log4j-slf4j-impl
${log4j2.version}
com.lmax
disruptor
${disruptor.version}
com.google.guava
guava
${guava.version}
javax.servlet
javax.servlet-api
${javax.servlet.version}
provided
org.hibernate
hibernate-validator
${hibernate-validator.version}
org.projectlombok
lombok
${lombok.version}
junit
junit
${junit.version}
test
com.alipay.sdk
alipay-sdk-java
3.7.4.ALL

open-api-web > pom

4.0.0
open-api-parent
com.open.api
1.0.0
com.open.api
open-api-web
${project.version}
Demo project for Spring Boot
com.open.api
open-api-common
${project.version}
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-log4j2
org.springframework.boot
spring-boot-test
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
org.apache.maven.plugins
maven-compiler-plugin
2.3.2
true
true
true
1.8
1.8
UTF-8

添加配置文件

server.port=8821#日志配置logging.level.root=WARNlogging.level.net.sf=WARNlogging.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@EnableAspectJAutoProxypublic 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@Componentpublic @interface OpenApiService {
}

API接口初始化容器与扫描器

/** * Api 初始化容器 */@Servicepublic class ApiContainer extends HashMap
{
}
/** * 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接口扫描器 */@Componentpublic 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
openApiServiceBeanMap = ApplicationContextHelper.getBeansWithAnnotation(OpenApiService.class); if (null == openApiServiceBeanMap || openApiServiceBeanMap.isEmpty()) {
LOGGER.info("open api service bean map is empty"); return; } for (Map.Entry
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
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 码农猿 */@Servicepublic 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
params, String requestRandomId, String charset, String signType) {
try {
//校验签名开关 if (!applicationProperty.getIsCheckSign()) {
LOGGER.warn("【{}】>> 验签开关关闭", requestRandomId); return; } //map类型转换 Map
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
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 * 

* 注解@OpenApiService > 开放接口自定义注解,用于启动时扫描接口 */@Servicepublic 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
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://www.cnblogs.com/mengq0815/p/10596068.html

你可能感兴趣的文章
selenium爬取网易云
查看>>
常用配置文件
查看>>
Python全栈之路系列之流程控制
查看>>
# 20155209 2016-2017-2 《Java程序设计》第六周学习总结
查看>>
shell 脚本获取数组字符串长度
查看>>
Spark性能优化指南——基础篇
查看>>
Adapter 适配器模式 MD
查看>>
Linux使用fdisk进行磁盘管理
查看>>
Linux设置服务自启动(转载)
查看>>
ASP.Net文件下载-使用流输出
查看>>
限定textbox中只能输入数字的小方法
查看>>
Android 手机app 嵌入网页操作
查看>>
Android:控件布局(表格布局)TableLayout
查看>>
VMWare Workstation虚拟机网卡工作模式及配置方法
查看>>
开始学习Angular Mobile UI
查看>>
浅谈C语言中的联合体
查看>>
Photoshop独立安装包下载页面
查看>>
使用git获取远程分支
查看>>
.Net开发之Request处理
查看>>
看了才知道!伊朗黑客组织原来这么牛
查看>>