# ycAPI平台
**Repository Path**: yichenwun/yc-app-platform
## Basic Information
- **Project Name**: ycAPI平台
- **Description**: 一个API开放平台
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-07-29
- **Last Updated**: 2024-09-05
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## API 开放平台
---
#### 一、项目概述
> 本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。
#### 二、项目介绍
**一个提供 API 接口供开发者调用的平台:**
管理员可以发布接口,同时统计分析各接口的调用情况,用户可以注册登录并开通接口调用权限,浏览接口以及在线进行调试,并可以使用 **SDK** 轻松地在代码中调用接口。
**使用自己开发的客户端 SDK,一行代码调用接口:**

#### 三、项目业务流程
##### 流程概述
**5个子系统:**
1. **模拟接口系统**:提供各种模拟接口供开发者使用和测试,例如,提供一个随机头像生成接口。
2. **后台管理系统**:管理员可以发布接口、设置接口的调用数量、设定是否下线接口等功能,以及查看用户使用接口的情况,例如使用次数,错误调用等。
3. **用户前台系统**:提供一个访问界面,供开发者浏览所有的接口,可以购买或开通接口,并获得一定量的调用次数。
4. **API 网关系统**:负责接口的流量控制,计费统计,安全防护等功能,提供一致的接口服务质量,和简化 API 的管理工作。
5. **第三方调用 SDK 系统**:提供一个简化的工具包,使得开发者可以更方便地调用接口,例如提供预封装的 HTTP 请求方法、接口调用示例等。
**关键问题:**
1. **接口设计**:需要设计清晰易用的 API 接口,并且提供详细的接口文档,以方便开发者使用。
2. **性能和可用性**:平台需要承载大量的接口请求,因此需要考虑到性能和可用性问题。例如,设计高效的数据存储和检索策略,确保 API 网关的高性能等。
3. **安全**:平台需要防止各种安全攻击,例如 DDOS 攻击,也需要保护用户的隐私和数据安全。
4. **计费和流量控制**:需要设计合理的计费策略和流量控制机制,以确保平台的稳定运行和收入来源。
5. **易用性和用户体验**:需要为开发者提供简单易用的接口调用工具和友好的用户界面,提供优质的用户体验。
##### 架构图
**什么是 SDK?**
>SDK 是软件开发工具包的缩写,是一种为软件开发者提供支持的一系列工具、接口和规范的集合。举个例子,比如腾讯云提供了一些接口,比如创建 VPC 等,如果开发者直接向腾讯云服务器发请求,需要输入密钥、做签名认证等操作,非常繁琐。因此,在构建第三方 API 平台时,一般都会提供一套 SDK,**让使用者能够轻松地调用接口,无需自己编写和封装 HTTP 请求**。可以把 SDK 理解为 Java 语法中的工具包,使用者只需要最少量的代码即可调用接口
#### 四、项目选型
- Java Spring Boot
- MySQL 数据库
- MyBatis-Plus 及 MyBatis X
- API 签名认证(Http 调用)
- Spring Boot Starter(SDK 开发)
- Dubbo 分布式(RPC、Nacos)
- Swagger + Knife4j 接口文档生成
- Spring Cloud Gateway 微服务网关
- Hutool、Apache Common Utils、Gson 等工具库
#### 五、项目实现
##### 1. 后端项目初始化
**数据库表设计:**
```sql
-- 接口信息
create table if not exists `interface_info`
(
`id` bigint not null auto_increment primary key comment '主键',
`name` varchar(256) not null comment '名称',
`description` varchar(256) null comment '描述',
`url` varchar(512) not null comment '接口地址',
`requestParams` text 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 '接口信息';
```
------ 表crud补充
##### 2. 模拟 (新建) 接口项目
先来真实的发布一个给开发者使用的接口,创建一个模拟接口项目 ycAPI-interface
**提供三个不同种类的模拟接口:**
1. GET 接口
2. POST 接口(url 传参)
3. POST 接口(Restful)
```java
@RestController
@RequestMapping("name")
public class NameController {
@GetMapping("/")
public String getNameByGet(String name) {
return "GET 你的名字是" + name;
}
@PostMapping("/")
public String getNameByPost(@RequestParam String name) {
return "POST 你的名字是" + name;
}
@PostMapping("/user")
public String getUserNameByPost(@RequestBody User user) {
return "POST 用户名字是" + user.getUsername();
}
}
```
---- 测试调用接口
###### 2.1 调用接口的方式
HTTP 调用方式:
1. HttpClient
2. RestTemplate
3. 第三方库(OKHTTP、Hutool √ )
**采用 Hutool 实现:**
1. 创建一个`client层`:客户端层,负责与用户交互、处理用户请求,以及调用服务端提供的 API 接口等任务的部分。
```java
/**
* 调用第三方接口的客户端
*
*/
public class YcApiClient {
public String getNameByGet(String name) {
}
public String getNameByPost(@RequestParam String name) {
}
public String getUserNameByPost(@RequestBody User user) {
}
}
```
2. 调用外部接口
>支持 get 和 post 的请求
```java
GET:
// 最简单的HTTP请求,可以自动通过header等信息判断编码,不区分HTTP和HTTPS
String result1= HttpUtil.get("https://www.baidu.com");
// 当无法识别页面编码的时候,可以自定义请求页面的编码
String result2= HttpUtil.get("https://www.baidu.com", CharsetUtil.CHARSET_UTF_8);
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap paramMap = new HashMap<>();
paramMap.put("city", "北京");
String result3= HttpUtil.get("https://www.baidu.com", paramMap);
```
```java
POST:
HashMap paramMap = new HashMap<>();
paramMap.put("city", "北京");
String result= HttpUtil.post("https://www.baidu.com", paramMap);
```
>支持JSON类型的请求
```java
Restful请求:
String json = ...;
String result2 = HttpRequest.post(url)
.body(json)
.execute().body();
```
```java
/**
* 调用第三方接口的客户端
*/
public class YcApiClient {
public static final String GLOBAL_URL_PREFIX = "http://localhost:8112/api/name/";
public String getNameByGet(String name) {
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap paramMap = new HashMap<>();
paramMap.put("name", "yichen");
String result = HttpUtil.get(GLOBAL_URL_PREFIX, paramMap);
System.out.println("GET:" + result);
return result;
}
public String getNameByPost(@RequestParam String name) {
HashMap paramMap = new HashMap<>();
paramMap.put("name", "yichen");
String result = HttpUtil.post(GLOBAL_URL_PREFIX, paramMap);
System.out.println("POST:" + result);
return result;
}
public String getUserNameByPost(@RequestBody User user) {
String json = JSONUtil.toJsonStr(user.getUserName());
String url = GLOBAL_URL_PREFIX + "/user";
String result = HttpRequest.post(url)
.body(json)
.execute().body();
System.out.println("POST Restful:" + result);
return result;
}
}
```
---- 测试
##### 3. API 签名认证
>为接口设置保护措施,例如限制每个用户每秒只能调用十次接口,即实施请求频次的限额控制。如果在后期,你的业务扩大,可能还需要收费。因此,我们必须知道谁在调用接口,并且不能让无权限的人随意调用。
>现在,我们需要设计一个方法,来确定谁在调用接口。在我们之前开发后端时,我们会进行一些权限检查。例如,当管理员执行删除操作时,后端需要检查这个用户是否为管理员。那么,我们如何获取用户信息呢?是否直接从后端的 session 中获取?但问题来了,当我们**调用接口时**,**我们有 session 吗?**比如说,我是前端直接发起请求,**我没有登录操作,我没有输入用户名和密码,我怎么去调用呢?**因此,一般情况下,我们会采用一个叫API签名认证的机制。
API 签名认证主要包括两个过程 :
1. 签发签名
2. 使用签名(校验签名)
这就像一些短信接口的 key 一样
>为什么我们需要API签名认证呢?
>
>第一,为了保证安全性,不能让任何人都能调用接口。那么,我们如何在后端实现签名认证呢?我们需要两个东西,即 accessKey 和 secretKey。这和用户名和密码类似,不过**每次调用接口都需要带上,实现无状态的请求。**这样,**即使你之前没来过,只要这次的状态正确,你就可以调用接口。**所以我们需要这两个东西来**标识用户**。
总结:
1. **保证安全性**,不能随便一个人调用
2. 适用于**无需保存登录态的场景**。只认签名,**不关注用户登录态**。
**签名认证实现**
通过 http **request header** 头传递参数。
- 参数 1:**accessKey**:调用的标识 userA, userB(复杂、无序、无规律)
- 参数 2:**secretKey**:密钥(复杂、无序、无规律)**该参数不能放到请求头中**
>为什么需要两个key?
>
>如果仅凭一个key就可以调用接口,那么**任何拿到这个key的人都可以无限制地调用这个接口**。这就好比,为什么你在登录网站时需要输入密码,而不是只输入用户名就可以了?
>其实这两者的原理是一样的。如果像token一样,一个key不行吗?token本质上也是不安全的,有可能会通过重放等等方式来攻破的。
(类似用户名和密码,区别:ak、sk 是无状态的)
**!!! 千万不能把密钥直接在服务器之间传递,有可能会被拦截**
- 参数 3:用户请求参数
- 参数 4:sign
**加密方式:**
对称加密、非对称加密、md5 签名(不可解密)
+ **用户参数 + 密钥** => **签名生成算法(MD5、HMac、Sha1)** => 不可解密的值
eg: abc + abcdefgh => sajdgdioajdgioa
**怎么知道这个签名对不对?**
服务端用一模一样的参数和算法去生成签名,只要和用户传的的一致,就表示一致。
**怎么防重放?**
- 参数 5:加 **nonce** 随机数,只能用一次
服务端要保存用过的随机数
- 参数 6:加 **timestamp** 时间戳,校验时间戳是否过期。
**API 签名认证是一个很灵活的设计,具体要有哪些参数、参数名如何一定要根据场景来。**
**(比如 userId、appId、version、固定值等)**
**实现:**
```java
-- 用户表
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
userName varchar(256) null comment '用户昵称',
userAvatar varchar(1024) null comment '用户头像',
userProfile varchar(512) null comment '用户简介',
userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban',
accessKey varchar(512) not null comment '通行证',
secretKey varchar(512) 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 '是否删除',
index idx_Id (id)
) comment '用户' collate = utf8mb4_unicode_ci;
```
>我们需要获取用户传递的 accessKey 和 secretKey。对于这种数据,建议不要直接在 URL 中传递,而是选择在请求头中传递会更为妥当。因为 **GET 请求的 URL 存在最大长度限制**,如果你传递的其他参数过多,可能会导致关键数据被挤出。因此,建议**从请求头中**获取这些数据。
1. 根据提供的 Key 去数据库中查询,检查此 Key 是否已经被分配过,以及关联的用户是否被禁用、是否合法。现在我们只是简单模拟这一过程。
```java
@RestController
@RequestMapping("/name")
public class NameController {
@PostMapping("/user")
public String getUserNameByPost(@RequestBody User user, HttpServletRequest request) {
// todo: 从数据库中取出,这里写死为简单模拟
String accessKey = request.getHeader("accessKey");
String secretKey = request.getHeader("secretKey");
if (!accessKey.equals("yichen") || !secretKey.equals("yichenwu")){
throw new RuntimeException("无权限");
}
return "POST 用户名字是" + user.getUserName();
}
}
```
2. 改造一下 YuApiClient.java,发请求可以带上 header
```java
public String getUserNameByPost(@RequestBody User user) {
String json = JSONUtil.toJsonStr(user.getUserName());
String url = GLOBAL_URL_PREFIX + "/user";
String result = HttpRequest.post(url)
.addHeaders(getHeaderMap())
.body(json)
.execute().body();
System.out.println("POST Restful:" + result);
return result;
}
// 创建一个私有方法,用于构造请求头
private Map getHeaderMap() {
// 创建一个新的 HashMap 对象
Map hashMap = new HashMap<>();
// 将 "accessKey" 和其对应的值放入 map 中
hashMap.put("accessKey", accessKey);
// 将 "secretKey" 和其对应的值放入 map 中
hashMap.put("secretKey", secretKey);
// 返回构造的请求头 map
return hashMap;
}
```
目前为止的安全性问题 1:
>我们的请求有可能被人拦截,我们将密码放在**请求头中**,如果有中间人拦截到了你的请求,他们就可以直接**从请求头中获取你的密码,然后使用你的密码发送请求。**
解决:
> 在标准的 API 签名认证中,我们需要**传递一个签名**。通常我们**不是直接将其传递给后台**,而是**根据该密钥生成一个签名**。**因为密码不能直接在服务器中传递,有可能会被拦截。**
>
> 所以我们需要对该密码进行**加密**,这里通常称之为签名。我们可以将用户传递的参数(例如ABC参数)与该密钥拼接在一起,然后使用签名算法进行加密。
>
>
>
> 我们的加密算法可以分为:
>
> 单向加密(md5 签名)、对称加密、非对称加密。
>
> 对称加密 : 它是分配一组密钥,你可以加密和解密。
>
> 非对称加密 : 你可以使用公钥加密,私钥解密,有些情况下也可以使用私钥加密,公钥解密。
>
> **单向加密 : 加密后无法解密。这种是安全性最高的**
>
>
>
> **验证:再次使用相同的参数进行生成,并与你传递的参数进行对比,看它们是否一致。**
问题 2: 重放 (重放XHR)
>不要相信前端加密。前端的加密是有用的,但是前端的加密不能完全保证安全,所以不要依赖前端。之前说过**所有的请求都是可以重放的**。也就是说,**无法直接加密,无论你如何加密,只要被人拦截了,他们只需使用你传递的加密内容再次发送给后台,结果是一样的**。因此,实际上也不安全。我们现在讨论的这种方法还不是很安全,还存在更复杂的问题。
>
>假设你的电脑使用了代理、代理服务,请小心操作。在这种情况下,减少使用一些乱七八糟的操作,尤其是在公司进行开发时尽量少用代理。
>
>如果是一个代理软件,他如果想搞你的话,你只要发出请求走了他的代理,他点一下重放就好了。
>
>**添加时间戳也不能防止重放攻击**
解决:
>第一种方式是通过**加入一个随机数实现标准的签名认证**。
>
>每次请求时,发送一个随机数给后端。**后端只接受并认可该随机数一次**,一旦随机数被使用过,后端将不再接受相同的随机数。
>
>这种方式解决了请求重放的问题,然而,这种方法**需要后端额外开发**来保存已使用的随机数。并且,**如果接口的并发量很大,每次请求都需要一个随机数,那么可能会面临处理百万、千万甚至亿级别请求的情况。**因此,除了使用随机数之外,我们还**需要其他机制来定期清理已使用的随机数**。
>
>
>
>第二种方式是**加入一个时间戳(timestamp)**。
>
>每个请求在发送时携带一个时间戳,并且后端会**验证该时间戳是否在指定的时间范围内**,例如不超过10分钟或5分钟。
>
>这可以**防止对方使用昨天的请求在今天进行重放**。
>
>通过这种方式,我们**可以一定程度上控制随机数的过期时间**。因为后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端会拒绝该请求。因此,**时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下,这两种方法可以相互配合使用。**
因此,在**标准的签名认证算法中**,建议至少添加以下五个参数:==accessKey、secretKey、sign、nonce(随机数)、timestamp(时间戳)==。
此外,建议将用户请求的其他参数,例如接口中的 name 参数,**也添加到签名中,以增加安全性**。
💡 类似于 HTTPS 协议,**签名认证的本质是确保密码不在服务器之间传输**。因为**任何在服务器之间传输的内容都有可能被拦截。**所以,请记住==密码绝不能在服务器之间传输==。不能错误地认为密码可以在前端传输,千万不要这样做,不要在前端调用时传输这些**敏感信息**。
##### 4. 开发 SDK
>对于具体的随机数生成和签名生成过程,开发者有必要关心吗?显然是不需要的。如果每次都要求开发者编写这么多代码,肯定会让他们感到沮丧😢。因此,我们需要为开发者提供一个易于使用的 SDK,使其能够便捷地调用接口。
**为什么需要 Starter?**
理想情况:开发者只需要关心调用哪些接口、传递哪些参数,就跟调用自己写的代码一样简单。
**开发 starter 的好处**:开发者引入之后,可以直接在 **application.yml 中写配置**,自动创建客户端。
**进一步说明:**
为了方便开发者的调用,我们不能让他们每次都自己编写签名算法,这显然很繁琐。因此,我们需要开发一个简单易用的 SDK,使开发者只需关注调用哪些接口、传递哪些参数,就像调用自己编写的代码一样简单。实际上,RPC(远程过程调用)就是为了实现这一目的而设计的。类似的例子是小程序开发或者调用第三方 API,如腾讯云的 API,它们都提供了相应的 SDK。
1. **创建SDK项目**
2. **删掉 maven 构建项目的方式,我们现在是要构建依赖包,而不是直接运行 jar 包的项目**
3. 我们希望用户能够通过引入 starter 的方式直接使用客户端,而不需要手动创建,所以我们需要**编写一个配置类**
4. 给用户**提供 ApiClient**,把 yuapi-interface 项目中的 client包、model包、utils包复制。
5. 去掉多余的 spring **注解**
6. 生成**客户端**的方法
```java
@Configuration
// 能够读取application.yml的配置,读取到配置之后,把这个读到的配置设置到我们这里的属性中,
// 这里给所有的配置加上前缀为"ycapi.client"
@ConfigurationProperties("ycapi.client")
@Data
// @ComponentScan 注解用于自动扫描组件,使得 Spring 能够自动注册相应的 Bean
@ComponentScan
public class YcApiClientConfig {
private String accessKey;
private String secretKey;
@Bean
public YcApiClient ycApiClient() {
return new YcApiClient(accessKey, secretKey);
}
}
```
7. 在 resources 目录下创建一个目录`META-INF` ( 注意要大写 )
在`META-INF`目录创建一个文件`spring.factories`
在 `spring.factories`文件内编写配置项为自动引入配置的类。

>上述配置项指定了要自动配置的类 com.yichen.ycapiclientSDK.YcApiClientConfig,它是我们刚刚编写的配置类。通过在 spring.factories 文件中配置我们的配置类,Spring Boot 将会在应用启动时自动加载和实例化 YuApiClientConfig,并将其应用于我们的应用程序中。这样,我们就可以使用自动配置生成的 ycApiClient 对象,而无需手动创建和配置。
8. 安装为本地的依赖 (下在了本地的maven仓库中)
9. 把**模拟接口**中之前的 client包、model包、utils包全删了
10. 引入我们写好的 SDK 依赖、
```xml
com.yichen
ycAPI-client-SDK
0.0.1
```
11. **模拟接口**中 application.yml 进行配置,能够自动识别 SDK 中的配置
------ 测试
之所以能够在写配置时看到提示,就是因为这个 spring-configuration-metadata.json
这东西怎么生成的呢?
##### 5. 接口发布和下线功能
**发布接口(仅管理员可操作)**
1. 校验该接口是否存在
2. 判断该接口是否可以调用
3. 修改接口数据库中的状态字段为 1
```java
/**
* 发布接口
*
* @param onOffLineInterfaceRequest
* @return
*/
@PostMapping("/onLine")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse onLineInterfaceInfo(@RequestBody OnOffLineInterfaceRequest onOffLineInterfaceRequest) {
if (onOffLineInterfaceRequest == null || onOffLineInterfaceRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 1. 校验该接口是否存在
InterfaceInfo interfaceInfo = interfaceInfoService.getById(onOffLineInterfaceRequest.getId());
ThrowUtils.throwIf(ObjectUtils.isEmpty(interfaceInfo), ErrorCode.PARAMS_ERROR, "接口不存在");
// 2. 判断该接口是否可以调用
// (这里先模拟一下,搞个假数据)
com.yichen.ycapiclientSDK.model.User user = new com.yichen.ycapiclientSDK.model.User();
user.setUserName("逸尘");
String userNameByPost = ycApiClient.getUserNameByPost(user);
ThrowUtils.throwIf(StringUtils.isBlank(userNameByPost), ErrorCode.NOT_FOUND_ERROR, "接口验证失败");
// 3. 修改接口数据库中的状态字段为 1
interfaceInfo.setStatus(InterfaceStatusEnum.ONLINE.getValue());
boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}
```
**下线接口(仅管理员可操作)**
1. 校验该接口是否存在
2. 修改接口数据库中的状态字段为 0
##### 6. 后端开发申请签名 (在线调用)
在用户注册impl中添加 accessKey,secretKey 生成规则
```java
String accessKey = DigestUtils.md5DigestAsHex((SALT + userAccount + RandomUtil.randomNumbers(4)).getBytes());
String secretKey = DigestUtils.md5DigestAsHex((SALT + userAccount + RandomUtil.randomNumbers(4)).getBytes());
```
💡 在线调用中有一个**关键点**,那就是确定前端向后端发送请求时所需的一些信息,例如请求参数的类型。直接使用 JSON 类型即可。
7. 后端在线接口调用**流程** (**如何将这些请求传递给真实的第三方接口呢?**)
>前端在调用接口时,首先将要调用的接口以及请求参数传递给后端,然后后端作为一个中转角色,再向模拟接口发送请求。除了中转功能,后端可能还需要进行一些判断,例如判断前端的测试频率是否过高,或者判断前端是否有权限进行该接口的测试。
在用户进行测试调用时,我们需要告知后端用户的签名信息,这样我们才能判断用户是否具有调用接口的权限。
在这里有三种考虑方式,具体取决于你对用户体验的考虑:
- 第一种方式:要求用户**必须具有接口权限**才能进行调用。
- 第二种方式:即使用户没有权限,也允许其进行调用,以便体验接口功能。如果选择进行体验,建议为用户**分配临时的签名**,类似于测试环境,**给予一定数量的调用次数**。这可能需要新增两个字段,例如在数据库中添加一个测试次数字段,稍微复杂一些。
- 第三种方式:可以**直接为每个用户提供几十次调用机会**,这样就简单了。假设你本来给用户提供了 1 万次调用次数,现在你可以直接给每个用户送 50 次。这样做起来更方便,这种方式非常巧妙。
```java
/**
* 在线调用接口
*
* @param interfaceInfoInvokeRequest
* @return
*/
@PostMapping("/invoke")
public BaseResponse