跳到主要内容

API 开放平台的搭建与接口功能管理

在当今的软件开发中,API 开放平台扮演着至关重要的角色,它为前端开发提供了后台接口的支持,同时也使得其他系统能够便捷地使用现有的功能。本文将详细介绍 API 开放平台的搭建过程以及接口功能管理的实现。

项目背景

该平台旨在为用户提供便捷的 API 接口调用服务,同时确保平台的安全性、稳定性和可管理性。用户可以在平台上开通接口调用权限,使用所需的接口,并对每次调用进行统计。管理员则拥有发布接口、下线接口、接入接口以及可视化接口调用情况和数据的权限。要具备的安全保障如下:

  1. 防止攻击:平台采用多种安全措施来防止各类攻击,如防火墙、入侵检测系统、加密传输等,确保平台和用户数据的安全。
  2. 限制与开通:严格控制接口的调用权限,只有经过授权的用户才能调用相应的接口,避免接口被随意调用,从而保障系统的稳定性和安全性。
  3. 统计调用次数:通过对接口调用次数的统计,管理员可以及时发现异常调用情况,如频繁的恶意调用,从而采取相应的措施进行防范。
  4. 计费功能:根据用户的接口调用情况进行计费,一方面可以合理分配资源,另一方面也可以促使用户更加合理地使用接口。
  5. 流量保护:设置流量限制,防止因流量过大导致系统崩溃或性能下降,确保平台的稳定运行。
  6. API 接入安全:在接入外部 API 时,进行严格的安全评估和验证,确保接入的 API 不会对平台和用户造成安全威胁。

平台功能

  1. 接口调用权限管理:用户可以根据自身需求申请开通接口调用权限,平台会对用户的权限进行严格管理,确保只有授权用户能够调用相应的接口。
  2. 接口使用与统计:用户在使用接口时,平台会对每次调用进行详细记录,包括调用时间、调用参数、返回结果等信息。这些统计数据将有助于管理员了解接口的使用情况,为优化和改进接口提供依据。
  3. 管理员功能:
    • 发布接口:管理员可以将新开发的接口发布到平台上,供用户使用。
    • 下线接口:对于不再需要或存在问题的接口,管理员可以将其下线,以确保系统的稳定性和安全性。
    • 接入接口:平台支持接入外部系统的接口,实现资源的整合和共享。
    • 可视化接口调用情况与数据:管理员可以通过可视化界面直观地查看接口的调用情况,包括调用次数、调用频率、响应时间等指标,同时还能查看接口返回的数据,以便及时发现问题并进行处理。

技术选型

前端使用 Ant Design Pro、React、Ant Design Procomponents、Umi 和 Umi Request(Axios 的封装)等技术;

后端采用 Java SpringBoot,并结合 Spring Boot Starter(SDK 开发)、网关、限流和日志等功能。

数据库设计

接口信息表interface_info,用于存储接口的相关信息,包括名称、描述、地址、请求头、响应头、状态、方法、创建人、是否删除、创建时间和更新时间等字段。

字段类型说明
idbigint主键id
namevarchar(256)名称
descriptionvarchar(256)描述
urlvarchar(512)接口地址
requestHeadertext请求头
responseHeadertext响应头
statusint接口状态0-关闭1-开启
methodvarchar(256)请求类型
userIdbigint创建人
isDeletetinyint是否删除 0-未删除 1-删除
createTimedatetime创建时间
updateTimedatetime更新时间

SQL语句如下:

-- auto-generated definition
create table user
(
id bigint auto_increment comment 'id'
primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
unionId varchar(256) null comment '微信开放平台id',
mpOpenId varchar(256) null comment '公众号openId',
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',
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 '是否删除'
)
comment '用户' collate = utf8mb4_unicode_ci;

create index idx_unionId
on user (unionId);

接口功能管理

  1. 接口发布 / 下线:
    • 发布接口:首先校验接口是否存在,然后判断接口是否可用,最后修改数据库中的状态字段为 1。
    • 下线接口(仅管理员):同样需要检验接口是否存在,然后将状态字段修改为 0。
  2. 浏览接口 / 查看接口文档,申请签名:
    • 主页接口浏览页面:用户可以在主页浏览接口信息,并通过链接查看具体接口的文档。
    • 查看接口文档:在查看接口文档页面,用户可以获取接口的详细信息,包括状态、描述、请求地址、方法、参数、头信息、创建和更新时间等。
    • 分配签名:在注册时为用户分配 accessKey 和 secretKey,用户也可以申请更换签名。
  3. 在线调试:用户可以在前端页面输入请求参数,进行在线调试,查看接口的返回结果。

通过以上功能的实现,我们的 API 开放平台能够满足用户对接口的管理和使用需求,同时也为管理员提供了便捷的接口管控手段。

接口发布/下线

发布接口 :

  1. 校验接口是否存在
  2. 判断接口是否可用
  3. 修改数据库中的状态字段为1

下线接口(仅管理员)

  1. 检验接口是否存在
  2. 修改状态字段为0

后端代码 :

    /**
* 发布
*
* @param idRequest
* @param request
* @return
*/
@PostMapping("/online")
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> onlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
if (idRequest == null || idRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = idRequest.getId();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 判断该接口是否可以调用
com.yunfei.yunfeiapiclientsdk.model.User user = new com.yunfei.yunfeiapiclientsdk.model.User();
user.setUsername("test");
String username = yunfeiapiClient.getUsernameByPost(user);
if (StringUtils.isBlank(username)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "接口验证失败");
}
// 仅本人或管理员可修改
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
interfaceInfo.setStatus(InterfaceInfoStatusEnum.ONLINE.getValue());
boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}

/**
* 下线
*
* @param idRequest
* @param request
* @return
*/
@PostMapping("/offline")
@AuthCheck(mustRole = "admin")
public BaseResponse<Boolean> offlineInterfaceInfo(@RequestBody IdRequest idRequest,
HttpServletRequest request) {
if (idRequest == null || idRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = idRequest.getId();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 仅本人或管理员可修改
InterfaceInfo interfaceInfo = new InterfaceInfo();
interfaceInfo.setId(id);
interfaceInfo.setStatus(InterfaceInfoStatusEnum.OFFLINE.getValue());
boolean result = interfaceInfoService.updateById(interfaceInfo);
return ResultUtils.success(result);
}

浏览接口 /查看接口文档,申请签名

主页接口浏览页面:

const Index: React.FC = () => {
const [loading, setLoading] = useState(false);
const [list, setList] = useState<API.InterfaceInfo[]>([]);
const [total, setTotal] = useState<number>(0);

const loadData = async (current = 1, pageSize = 5) => {
setLoading(true);
try {
const res = await listInterfaceInfoByPageUsingGET({
current,
pageSize,
});
setList(res?.data?.records ?? []);
setTotal(res?.data?.total ?? 0);
} catch (error: any) {
message.error('请求失败,' + error.message);
}
setLoading(false);
};

// 这个函数会在组件挂载后执行一次
useEffect(() => {
loadData();
}, []);

return (
<PageContainer title="在线接口开放平台">
<List
className="my-list"
loading={loading}
itemLayout="horizontal"
dataSource={list}
renderItem={(item) => {
const apiLink = `/interface_info/${item.id}`;
return (
<List.Item actions={[<a key={item.id} href={apiLink}>查看</a>]}>
<List.Item.Meta
title={<a href={apiLink}>{item.name}</a>}
description={item.description}
/>
</List.Item>
);
}}
pagination={{
// eslint-disable-next-line @typescript-eslint/no-shadow
showTotal(total: number) {
return '总数:' + total;
},
pageSize: 5,
total,
onChange(page, pageSize) {
loadData(page, pageSize);
},
}}
/>
</PageContainer>
);
};

页面效果 如下:

image.png

查看接口文档:

/**
* 主页
* @constructor
*/
const Index: React.FC = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<API.InterfaceInfo>();
const [invokeRes, setInvokeRes] = useState<any>();
const [invokeLoading, setInvokeLoading] = useState(false);

const params = useParams();

const loadData = async () => {
if (!params.id) {
message.error('参数不存在');
return;
}
setLoading(true);
try {
const res = await getInterfaceInfoByIdUsingGET({
id: Number(params.id),
});
setData(res.data);
} catch (error: any) {
message.error('请求失败,' + error.message);
}
setLoading(false);
};

useEffect(() => {
loadData();
}, []);

const onFinish = async (values: any) => {
if (!params.id) {
message.error('接口不存在');
return;
}
setInvokeLoading(true);
try {
const res = await invokeInterfaceInfoUsingPOST({
id: params.id,
...values,
});
setInvokeRes(res.data);
message.success('请求成功');
} catch (error: any) {
message.error('操作失败,' + error.message);
}
setInvokeLoading(false);
};

return (
<PageContainer title="查看接口文档">
<Card>
{data ? (
<Descriptions title={data.name} column={1}>
<Descriptions.Item label="接口状态">{data.status ? '开启' : '关闭'}</Descriptions.Item>
<Descriptions.Item label="描述">{data.description}</Descriptions.Item>
<Descriptions.Item label="请求地址">{data.url}</Descriptions.Item>
<Descriptions.Item label="请求方法">{data.method}</Descriptions.Item>
<Descriptions.Item label="请求参数">{data.requestParams}</Descriptions.Item>
<Descriptions.Item label="请求头">{data.requestHeader}</Descriptions.Item>
<Descriptions.Item label="响应头">{data.responseHeader}</Descriptions.Item>
<Descriptions.Item label="创建时间">{data.createTime}</Descriptions.Item>
<Descriptions.Item label="更新时间">{data.updateTime}</Descriptions.Item>
</Descriptions>
) : (
<>接口不存在</>
)}
</Card>
<Divider/>
<Card title="在线测试">
<Form name="invoke" layout="vertical" onFinish={onFinish}>
<Form.Item label="请求参数" name="userRequestParams">
<Input.TextArea/>
</Form.Item>
<Form.Item wrapperCol={{span: 16}}>
<Button type="primary" htmlType="submit">
调用
</Button>
</Form.Item>
</Form>
</Card>
<Divider/>
<Card title="返回结果" loading={invokeLoading}>
{invokeRes}
</Card>
</PageContainer>
);
};

export default Index;

分配签名: 在注册的时候分配用户的accessKey,secretKey

            // 3. 分配 accessKey, secretKey
String accessKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(5));
String secretKey = DigestUtil.md5Hex(SALT + userAccount + RandomUtil.randomNumbers(8));
// 4. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
user.setAccessKey(accessKey);
user.setSecretKey(secretKey);
boolean saveResult = this.save(user);

扩展:用户可以申请更换签名

在线调试

请求参数的类型:

[
{"name":"username","type":"string"}
]

前端开发:

const Index: React.FC = () => {  
const [loading, setLoading] = useState(false);
const [data, setData] = useState<API.InterfaceInfo>();
const [invokeRes, setInvokeRes] = useState<any>();
const [invokeLoading, setInvokeLoading] = useState(false);

const params = useParams();

const loadData = async () => {
if (!params.id) {
message.error('参数不存在');
return; }
setLoading(true);
try {
const res = await getInterfaceInfoByIdUsingGET({
id: Number(params.id),
});
setData(res.data);
} catch (error: any) {
message.error('请求失败,' + error.message);
}
setLoading(false);
};

useEffect(() => {
loadData();
}, []);

const onFinish = async (values: any) => {
if (!params.id) {
message.error('接口不存在');
return; }
setInvokeLoading(true);
try {
const res = await invokeInterfaceInfoUsingPOST({
id: params.id,
...values,
});
setInvokeRes(res.data);
message.success('请求成功');
} catch (error: any) {
message.error('操作失败,' + error.message);
}
setInvokeLoading(false);
};

return (
<PageContainer title="查看接口文档">
<Card> {data ? (
<Descriptions title={data.name} column={1}>
<Descriptions.Item label="接口状态">{data.status ? '开启' : '关闭'}</Descriptions.Item>
<Descriptions.Item label="描述">{data.description}</Descriptions.Item>
<Descriptions.Item label="请求地址">{data.url}</Descriptions.Item>
<Descriptions.Item label="请求方法">{data.method}</Descriptions.Item>
<Descriptions.Item label="请求参数">{data.requestParams}</Descriptions.Item>
<Descriptions.Item label="请求头">{data.requestHeader}</Descriptions.Item>
<Descriptions.Item label="响应头">{data.responseHeader}</Descriptions.Item>
<Descriptions.Item label="创建时间">{data.createTime}</Descriptions.Item>
<Descriptions.Item label="更新时间">{data.updateTime}</Descriptions.Item>
</Descriptions> ) : (
<>接口不存在</>
)}
</Card>
<Divider/> <Card title="在线测试">
<Form name="invoke" layout="vertical" onFinish={onFinish}>
<Form.Item label="请求参数" name="userRequestParams">
<Input.TextArea/> </Form.Item> <Form.Item wrapperCol={{span: 16}}>
<Button type="primary" htmlType="submit">
调用
</Button>
</Form.Item> </Form> </Card> <Divider/> <Card title="返回结果" loading={invokeLoading}>
{invokeRes}
</Card>
</PageContainer> );
};

export default Index;

效果:

image.png

优化:可以做类似knife4j的效果

在线调试后端:

    /**
* 测试调用
*
* @param interfaceInfoInvokeRequest
* @param request
* @return
*/
@PostMapping("/invoke")
public BaseResponse<Object> invokeInterfaceInfo(@RequestBody InterfaceInfoInvokeRequest interfaceInfoInvokeRequest,
HttpServletRequest request) {
if (interfaceInfoInvokeRequest == null || interfaceInfoInvokeRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
long id = interfaceInfoInvokeRequest.getId();
String userRequestParams = interfaceInfoInvokeRequest.getUserRequestParams();
// 判断是否存在
InterfaceInfo oldInterfaceInfo = interfaceInfoService.getById(id);
if (oldInterfaceInfo == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
if (oldInterfaceInfo.getStatus() == InterfaceInfoStatusEnum.OFFLINE.getValue()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "接口已关闭");
}
User loginUser = userService.getLoginUser(request);
String accessKey = loginUser.getAccessKey();
String secretKey = loginUser.getSecretKey();
YunfeiApiClient tempClient = new YunfeiApiClient(accessKey, secretKey);
Gson gson = new Gson();
com.yunfei.yunfeiapiclientsdk.model.User user = gson.fromJson(userRequestParams, com.yunfei.yunfeiapiclientsdk.model.User.class);
String usernameByPost = tempClient.getUsernameByPost(user);
return ResultUtils.success(usernameByPost);
}