本文主要讲解开放接口设计,主要是以SpringBoot web 项目,基于自定义注解+反射+非对称加密RSA签名等实现的灵活的统一开放接口设计,文末附源码地址。
互联网公司随着业务的发展,系统间或多或少会开放一些对外接口,这些接口都会以API的形式提供给外部。
为了方便统一管理,统一鉴权,统一签名认证机制,流量预警等引入了统一网关。API网关是一是对外接口唯一入口。
除了业务功能还需要有
对外开放的接口,数据安全性是第一位
比较流行的方式一般是
# 3.开放接口设计 本文用到的主要技术点
公共参数
参数
类型
是否必填
最大长度
描述
app_id
String
是
32
业务方appId
method
String
是
200
请求方法
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时必填)
**
这里是使用的是RSA签名
规则如下:
RSA === SHA1 --> base64
RSA2 === SHA256 --> base64
注:源码见文章末,依赖这里只简单列出了父pom中部分依赖,,详见源码
工程结构图:
注:依赖这里只列举了一部分,文末附源码地址
open-api-project > pom
<properties>
<project.version>1.0.0</project.version>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<fasterxml.version>2.10.2</fasterxml.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-collections.version>3.2.2</commons-collections.version>
<fastjson.version>1.2.70</fastjson.version>
<slf4j.version>1.7.25</slf4j.version>
<guava.version>21.0</guava.version>
<javax.servlet.version>3.1.0</javax.servlet.version>
<alipay-sdk.version>4.8.10.ALL</alipay-sdk.version>
<hutool.version>5.0.0</hutool.version>
<lombok.version>1.16.4</lombok.version>
<hibernate-validator.version>5.4.1.Final</hibernate-validator.version>
</properties>
<!-- 依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${fasterxml.version}</version>
</dependency>
<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>
<!--fastjson json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- 校验工具 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
open-api-web > pom
<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>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</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>
server.port=8821
#日志配置
logging.level.root=WARN
logging.level.net.sf=WARN
logging.level.com.open.api=debug
#是否校验签名
open.api.common.key.isCheckSign=true
#开放接口公钥
open.api.common.key.publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwdK0le7UypaYWEWBoQkGTpu2nlYnM+iX8pa7Gz9neSnANfcuxrMgmmXrG+Dw6f3OQpiHl4mbKelyVjJTBLh4cvo1am2OSZvBjefZNshphx4ctBtx6BpGIRwlTvJRsjajMCY3RyF6px+Ehz0zeDBf7w2M6GZnSv2YhPp2YIZZo/01GYVJ4RgzzfkEEKyC+96+shqANHVOaiiG4byMJL8zv9q3kshSNCA1NT8r7toq8wPYhUKwCas/i5GauyRCIX+KhCpD9+/HTkFmr0PUWoNIZ61lRpTMbiTfDWU/5tJ3UPwdk6oVM3ZwkLoBAO8HXHvPk6avCupXq63u4wGrn30eiwIDAQAB
#开放接口私钥
open.api.common.key.privateKey=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDB0rSV7tTKlphYRYGhCQZOm7aeVicz6JfylrsbP2d5KcA19y7GsyCaZesb4PDp/c5CmIeXiZsp6XJWMlMEuHhy+jVqbY5Jm8GN59k2yGmHHhy0G3HoGkYhHCVO8lGyNqMwJjdHIXqnH4SHPTN4MF/vDYzoZmdK/ZiE+nZghlmj/TUZhUnhGDPN+QQQrIL73r6yGoA0dU5qKIbhvIwkvzO/2reSyFI0IDU1Pyvu2irzA9iFQrAJqz+LkZq7JEIhf4qEKkP378dOQWavQ9Rag0hnrWVGlMxuJN8NZT/m0ndQ/B2TqhUzdnCQugEA7wdce8+Tpq8K6lerre7jAauffR6LAgMBAAECggEBAIv3vF9V5KcUD5oXP6BqIvrbagp3zsGmoywVe7MWm4OdCeguw8HME6xME3fDflaL6cqf2bMuNTYUFnR2zQroqFrno3FjAlDXwPPYTT1JhyODNFlARIbHioNYjvyu8x5OZJRd1KdyXt+XXB5JrQSLcovwbiRZ5xf5gI3vTVMxUkSgTxf2P2smaXLZ2k6epSlvFr8u+SJaGOgjKCvbGf+jXyL0L8kntukNLocnSXU3sfFmAmd87DxPFdXAFDnS09tWOcjHfIZmwjHMX3qVP/2jj1DWOjIW0Ow/VRegbYTpLmSQTMcOUaFRprvwd0ZKaZ0aQMNPqPrqkHzrfQsnfjxY+akCgYEA+vbt40rIBDsN5HCPGbyDBU0/+A3wsGh4nqvY9JKACaMpg/FvyMz37GpL8AOMy/mUCVXjVyMoNUFZf/fEhMblBuYQBmgMVk1IQaVESvUlZ33Vgot0TU8YHY2Hpk541e3vKL0X0X6XLgS6CZ63cMx04uZxoFEWlJJm/qqLru3MQp8CgYEAxbZFVgnQ9XTQlHHgpUiS/R7qHo5joBDzlF+m29CYplI5nJUmntoChnHZ6RBoiPW58A3NJOlfIL6J2+Mwwd6kHWD2DSwVDRfk7Hb5Dw6o+tOf+In+zuPZtopr6L7oiKQtwXtGV88ZhDdqX9z5ge9EYP4Psd0Mfchv5vkJENreqJUCgYEAw8easTQHcXV4UvuURymOtLYc7zBA0f3OC0pYiAM5q0sD+hCBeg6cYmxSLT03u3BKEjZUkge1OEZwwanSPxrCVG1plvXYmgLUGZIKAsfXlDLQO3T7F8xaLcPZTN3u2kUxy4Aocp/k5Ft/nj2ZMX/ut4u6nKxlhyXm/0igi6irLlUCgYAWpalNkLRJ2Yam6mB0Llr/+ZGRzHem9yofndFMLpm9u39z6zXQTmKpqdLvOnzu607QK5SLHNxTsN+zu1NzcaBU6S1mFt2WcV08pOgkjGZYzPLvEkeIxVrD6RkxQOT7+epv1kIZftSKa5qYvoQqGRE5FwEPO6XZpqMCzxX1w0xr/QKBgDeom9MiI9a125jr/n9ghSWfvaxCXgPrdojr8QZrlo028iT711ND8QUwCbb9GDr+pyXesANCm78zhdltfeNFimEUktyS0F8Li2+GbYjTvLNXtwxTKZcOXRR+MC5bMHq4hY/+71NhnGazy3yidHn0doReezqGvkotJuRJSr+l1qmU
作用说明:自定义注解主要用于标记开放接口
开放接口方法注解
/**
* 开放接口注解
*
* @author 程序员小强
*/
@Documented
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApi {
/**
* api 方法名
*/
String method();
/**
* 方法描述
*/
String desc() default "";
}
开放接口实现类注解
/**
* 开放接口实现类注解
*
* @author 程序员小强
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OpenApiService {
}
作用说明:在项目启动的时候,将添加自定义注解的实现加载到容器
/**
* 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
}
//导包 太长,这里省略了,详见源码
/**
* Api接口扫描器
*
* @author 程序员小强
*/
@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;
}
}
作用说明:统一网关controller中调用一下方法进行验签与执行相应的业务方法。
//导包 太长,这里省略了,详见源码
/**
* 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);
}
}
}
/**
* 统一网关平台-启动类
*
* @author 程序员小强
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
/**
* 统一异常处理
*
* @author 程序员小强
*/
@Slf4j
@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;
}
}
//导包 太长,这里省略了,详见源码
/**
* 统一网关
*/
@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;
}
}
入参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
@OpenApiService
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));
}
}
访问地址:http://localhost:8821/open/gateway
注:为了方便调试先将配置文件中的验签开关修改为 false
open.api.common.key.isCheckSign=false
正常访问
异常情况
将配置文件验签开关开启
open.api.common.key.isCheckSign=true
由于签名是根据私钥+动态的参数根据规则生成的,在PostMan上测试还得先生成签名,在源码文件中有一个测试Demo如下**
带签名正常访问
签名异常情况下访问
目前配置的参数入参默认是下划线方式,如果业务中要用驼峰,可以改以下方法。
因为使用反射方式调用方法,所以自定义接口的时候也需要遵循规范,参数为2个
参数1 String requestId,
参数2 业务类型对象参数,实际是 网关入口,content 内容JSON转换后的对象参数。
源码地址:传送门
关注程序员小强公众号更多编程趣事,知识心得与您分享
原网址: 访问
创建于: 2021-08-26 11:28:43
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论