# API-Platform **Repository Path**: china-sun/api-platform ## Basic Information - **Project Name**: API-Platform - **Description**: API 开放平台 - **Primary Language**: Unknown - **License**: Zlib - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-04-14 - **Last Updated**: 2025-04-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [1]:https://pro.ant.design/zh-CN/ "ANT Design Pro" [2]:https://zh-hans.react.dev/learn "React" [3]:https://blog.luoaicheng.cn/content/108/#%E6%90%8F%E5%A4%A9 # Introduce 你是一个前端开发同学,你不会后端开发,不了解后端有哪些接口。如果你获取到了后端的接口,则可以直接调用接口获取数据;前端开发效率会得到提升。另外,你还可能使用网上公开的网络接口获取来自三方平台的数据或者使用三方平台的功能,比如 [Sina-API](https://open.weibo.com/wiki/%E9%A6%96%E9%A1%B5) [免费随机图片 api 接口](https://blog.luoaicheng.cn/content/108/) 但我们在做这类平台时,需要考虑到 - DOS - 调用限制 - 统计/计费 - 流量保护 - API 接入 这里我们做一个提供 API 接口调用的平台,用户可以注册登录,开通接口调用权限。用户可以使用接口,并且每次调用会进行统计。管理员可以`发布` `下线` `接入` 以及可视化调用统计等。 ## 业务流程 ![image-20241015130500565](./assets/image-20241015130500565.png) 除了上述内容之外,还需要有提供使用方法的 API 文档。为了实现 统计/计费 等功能,可以使用 AOP ,但是为了使得这个内容与我们的项目本身分离开,这里使用微服务架构,这里引入一个 API 网关,用于控制哪些接口需要授权,哪些不需要。 为了方便开发者调用,还需要考虑发布一个接口调用的 SDK (Software Development Kit)。 ## 技术选型 本次项目内容是首次接触,所以难点是在`架构思想`上;本次项目就是对一种架构思想的实现。 前端使用 `Ant Design Pro`/`React`/`Ant Design Procomponents`/`Umi/Umi Request`;后端使用 `Spring Boot`/`Spring Cloud Gateway` 另外还需要开发一个 Spring Boot 的 starter 并发布到 mvn。 ## 需求分析 ## 数据库设计 | **接口信息表(interface_info)** | | | | ------------------------------ | -------------------------- | ------------ | | **字段** | **说明** | **类型** | | id | 用户 id(主键) | bigint | | name | 名称 | varchar(256) | | description | 描述 | varchar(256) | | url | 接口地址 | varchar(512) | | requestHeader | 请求头 | text | | responseHeader | 响应头 | text | | status | 接口状态(0-关闭,1-开启) | int | | method | 请求方法 | varchar(256) | | userId | 创建人 | bigint | | createTime | 创建时间 | datetime | | updateTime | 更新时间 | datetime | | isDelete | 是否删除(0-未删, 1-已删) | tinyint | ```sql create database if not exists open_api_platform; use open_api_platform; -- 接口信息 create table if not exists `interface_info` ( `id` bigint not null auto_increment comment '主键' primary key, `name` varchar(256) not null comment '名称', `description` varchar(256) null comment '描述', `url` varchar(512) not null comment '接口地址', `requestHeader` text null comment '请求头', `responseHeader` text null comment '响应头', `status` int default 0 not null comment '接口状态(0-关闭,1-开启)', `method` varchar(256) not null comment '请求类型', `userId` bigint not null comment '创建人', `createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', `isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)' ) comment '接口信息'; ``` # 接口调用 在 Java 端,可以使用 `RestTemplate` `Hutool` `java.net.HttpClient` 等工具进行 Http 请求;这里有一个问题是,如何保证安全性,不能随便一个客户端都可以进行 API 调用。一个业内广为使用的方法是 `Access-Key` `Secret-Key` ,就像用户名和密码,但是与之不同的是 `ak` `sk` 是无状态的,服务器不关心之前这一对 key 是否访问过,而用户名和密码则可能会根据用户需要保存登录时的状态以实现快速登录。 另外 `ak` `sk` 需要复杂,无规律;这一对key 的保存可以使用数据库,为 user 表新增两个字段 ``` sql alter table user add column `accessKey` varchar(512) not null comment "用于接口调用"; `secretKey` varchar(512) not null comment "用于接口调用"; ``` 显然,为了防止请求被拦截,ak/sk 被暴露,一定的加密是必须的。 为了防止客户端请求被重放,可以使用 `nonce` 机制,可以在请求时生成随机数,服务器保存这些随机数一定时间,在这一段时间内,这个随机数不能被重复使用。所以我们需要如下一些字段 ```java public static final String NONCE_HEADER = "X-Camellia-Nonce"; public static final String AK_SK_SIGN = "X-Camellia-AK-SK-Sign"; public static final String TIMESTAMP_HEADER = "X-Camellia-Timestamp"; ``` 对于这些内容的检测则注册到 `HandlerInterceptor` 中;此外,还需要将 `ak/sk` 进行签名,在签名过程中加入额外的 `SALT`;具体的签名计算如下 ```java private String encode(String accessKey, String secretKey) { StringBuilder builder = new StringBuilder(); builder.append(accessKey).append(secretKey).append(SALT); return new String(Base64.getEncoder().encode(instance.digest(builder.toString().getBytes()))); } ``` 服务端的 `match` 如下 ```java private boolean match(String sign, String accessKey, String secretKey) { StringBuilder builder = new StringBuilder(); builder.append(accessKey).append(secretKey).append(SALT); String userNonce = new String(Base64.getEncoder() .encode(instance.digest(builder.toString().getBytes()))); return userNonce.equals(sign); } ``` 在服务端,ak/sk 是需要从数据库中获取的。 ## SDK 开发 image-20241017162451173 为了可以使得我们定义的配置信息可以被引入者的 IDEA 提示,需要引入 `Spring Configuration Processor` 依赖 ![image-20241017162654983](./assets/image-20241017162654983.png) 按照规范,将 sdk 包定义为 `world.snowcrystal.spring`,定义的自动配置类如下 ```java @Configuration @ComponentScan @ConfigurationProperties("snowcrystal.client") @ConditionalOnBean(RestTemplate.class) public class ApiClientAutoConfiguration { private String secretKey; private String accessKey; @Bean public SnowCrystalApiClient snowCrystalApiClient() { return new SnowCrystalApiClient(new RestTemplate(), secretKey, accessKey); } } ``` 由于我们不使用 `@SpringBootApplication` 注解,所以 Bean 扫描需要使用 `ComponentScan` 最后,为了使得引入者的 SpringBoot 可以扫描到这个自动配置类,需要在 `META-INF/spring.factories` 中声明如下 ```properties org.springframework.boot.autoconfigure.EnableAutoConfiguration= \ world.snowcrystal.spring.autoconfiguration.ApiClientAutoConfiguration ``` 这个是为了兼容 SpringBoot 3 以下的版本,`spring.factories`功能在`Spring Boot 2.7`已经废弃,在`Spring Boot 3.0`彻底移除,如果你使用的是 SpringBoot 3+,则在类路径下创建`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`文件,文件的内容是:**每个实现类的全类名单独一行**。 ```imports world.snowcrystal.spring.autoconfiguration.ApiClientAutoConfiguration ``` 之后将 pom 中的以下内容删除 ```xml org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ``` 运行 mvn 声明周期 install # 接口计费/保护 计费功能主要还是收集用户接口的调用情况;用户每成功调用一次,调用次数 +1;需要给用户分配或者用户主动申请接口调用次数。虽然也可以使用 `-1` 的方法,但是我们需要统计用户总的调用次数,实际上两种方式均可,只不过注重点不同。 1. 用户调用接口(after success) 2. 修改数据库,调用次数 +1 在存储上,我们需要保存:哪个用户调用了哪个接口,一个用户可能调用多个接口,一个接口可能被多个用户调用。多对多,需要设计独立的表: ```sql create table if not exists `user_invoke_log` ( `id` bigint not null auto_increment primary key comment '主键', `userId` bigint not null comment '用户 id', `interfaceId` bigint not null comment '接口 id', `invokeCount` int default 0 not null comment '总的调用次数', `remainingInvokeTimes` int default 0 not null comment '剩余的调用次数,用户可以购买', `status` int default 0 not null comment '用户是否违法了某些规定而被禁止调用此接口;0表示没有,1表示有,为 1 时, 用户应不再被允许调用此接口', `createTime` datetime default CURRENT_TIMESTAMP not null comment '记录创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null comment '上一次更新时间', `isDelete` int default 0 not null comment '该记录是否已被标记删除' ); ``` 由于每个方法在被调用后都需要进行记录调用次数,所以可以使用 AOP 解决,但 AOP 只能使用于单个项目中,接口提供这都会有记录调用的需求;不可能要求都需要每个团队每个切面,因为这将导致每个团队都需要引入你的 Aspect 包。 所以,我们需要的是 AOP 的思想,而不是 AOP 本身,适用于项目级别的 AOP 切面,就是`网关` image-20241019123348088 1. 用户发送请求到 API 网关 2. 日志;请求染色 3. API 网关对用户鉴权 (AK/SK) 4. 判断请求的接口是否存在 5. 调用接口 6. 调用成功,接口次数 +1 || 调用失败,返回一个规范错误 7. 响应日志 在 ak/sk 鉴权部分,因为网关项目没有引入 MyBatis 等操作数据库的依赖,如果该操作较为复杂,则直接由负责增删改查的项目提供服务,网关直接调用。 在接口调用成功后,在网关的 Filter 中我们还有一些日志逻辑,可以使用 Post Filter 实现 spring_cloud_gateway_diagram pre / post filter 逻辑如下 ```java public class CustomGlobalFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // pre filter logics return chain.filter(exchange) .then(Mono.fromRunnable(() -> { // post filter logics })); } } ``` ## 网关如何复用已经存在的逻辑 网关需要对用户进行 aksk 认证,认证的逻辑已经写好在了后端 `starter` 中,除了引入 `starter` 包外,还有一些方式可以考虑 - 通过 HTTP 调用:后端服务不仅仅处理来自前端的请求,还开放了 HTTP 接口供网关调用 - RPC - 打包,网关添加依赖 #### 网关使用 OpenFeign 时出现的循环依赖问题 ```tex The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | filteringWebHandler defined in class path resource [org/springframework/cloud/gateway/config/GatewayAutoConfiguration.class] ↑ ↓ | customGlobalFilter ↑ ↓ | world.snowcrystal.commons.service.RpcInterfaceService ↑ ↓ | corsGatewayFilterApplicationListener defined in class path resource [org/springframework/cloud/gateway/config/GatewayAutoConfiguration.class] ↑ ↓ | routePredicateHandlerMapping defined in class path resource [org/springframework/cloud/gateway/config/GatewayAutoConfiguration.class] └─────┘ ``` 这并非是代码编排上的错误,这可以说是一种 BUG 了。我尝试了 5 种解决循环依赖的方法,由于 `RpcInterfaceService` 是动态生成的,所以不知道其是哪种注入方式, - × :customGlobalFilter 使用 `@Lazy` 注解,并设置 `@Autrowire` 构造注入; - × :customGlobalFilter 使用 `@Lazy` 注解,并设置 `@Autrowire` 进行 setter 注入; - × :customGlobalFilter 使用 `@DependsOn()` 注解 - × :customGlobalFilter 实现 `ApplicationContextAware` 以及 `InitializingBean` 接口,在通过 `ApplicationContextAware` 获取应用上下文后,使用 `InitializingBean` 的 `afterPropertiesSet` 进行手动注入。虽然这种方式使得 Spring 没有检测到循环依赖,但是循环依赖仍然确实存在,并使程序产生了 `StackOverflowException` - × :这个方法能解决问题,customGlobalFilter 使用 `@ConditionOn` 等条件注解;此时,customGlobalFilter 并不会被加载到 IoC 容器 - √ :最后,根据 IoC 容器的声明周期,使 customGlobalFilter 实现 `ApplicationListener` 接口,监听容器的 `ContextStoppedEvent` 事件,此时再获取应用上下文进行手动注入。 # 管理员统计与可视化 需求:获取某个用户调用各个接口的总次数占比,或者说接口的总调用次数占比;从而分析出哪些接口没有人用(降低资源或者下线),高频接口(增加资源提高收费) 后端需要些一个接口得到数据输;如果接口非常多,则只取前 10 个接口 ```sql select name,interfaceId,sum(invokeCount) as totalInvokeCount,count(*) from user_invoke_log left join interface_info on user_invoke_log.interfaceId = interface_info.id group by interfaceId order by totalInvokeCount desc limit 10; ``` 除此之外,关联查询接口信息 ## 每天的接口调用情况 ```sql create table if not exists `invoke_log_daily` ( `id` bigint auto_increment not null primary key comment '主键', `dateTime` datetime default CURRENT_TIMESTAMP not null comment '日期', `dailyInvokeCount` int default 0 not null comment '当日总调用次数', `createTime` datetime default CURRENT_TIMESTAMP not null comment '记录创建时间', `updateTime` datetime default CURRENT_TIMESTAMP not null comment '上一次更新时间', `isDelete` int default 0 not null comment '该记录是否已被标记删除' ) comment '每天接口的调用情况'; ```