API开放平台

背景:

  1. 前端开发需要用到后台接口

  2. 使用现成的系统的功能

做一个API接口平台:

  1. 防止攻击(安全性)
  2. 不能随便调用(限制、开通)
  3. 统计调用次数
  4. 计费
  5. 流量保护
  6. API接入

项目介绍

做一个提供API接口调用的平台:

  • 用户可以注册登录,开通接口调用权限。用户可以使用接口,并且每次调用会进行统计。

  • 管理员可以发布接口、下线接口、接入接口,以及可视化接口的调用情况、数据。

业务流程

image-20230711194146565

技术选型

前端

  • Ant Design Pro

  • React

  • Ant Design Procomponents

  • Umi

  • Umi Request(Axios的封装)

后端

  • Spring Boot

  • Spring Boot Starter(SDK开发)

  • 网关、限流、日志实现 —–> dubbo+nacos

项目开发的阶段划分

1、初始化和展示

项目介绍、设计、技术选型

基础项目的搭建

接口管理

用户查看接口

2、接口调用

  1. 继续开发接口管理前端页面
  2. 开发模拟API接口
  3. 开发调用这个接口的代码
  4. 保证调用的安全性(API签名认证)
  5. 客户端SDK的开发

3、接口计量与保护

  1. 开发接口发布/下线的功能(管理员)
  2. 前端去浏览接口、查看接口文档、申请签名(注册)、在线调试(用户)
  3. 统计用户调用接口次数
  4. 优化系统 - API网关
开发接口发布/下线的功能(管理员)
发布接口(仅管理员可用):
  1. 检查该接口是否存在
  2. 判断该接口是否可以调用
  3. 修改接口数据库中的状态字段为1
下线接口:
  1. 检查该接口是否存在
  2. 判断该接口是否可以调用
  3. 修改接口数据库中的状态字段为0

前端去浏览接口、查看接口文档

–动态路由,用url来传递id,加载不同的接口信息

申请签名(注册) :

用户在注册成功时,自动分配 accessKey、secretKey

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

在线调用

​ 请求参数的类型(直接用 json 类型)

1
2
3
[
{"name": "username", "type": "string"}
]

先跑通整个流程,再去针对不同的请求或接口类型来设计界面和表单,给用户更好的体验(可以参考postman)

调用流程:

使用走后端方式流程:(后端相当于中转站,防止用户直接拿到接口的地址,从而绕过系统自行调用)

image-20230716140059896

流程:

1. 前端将用户输入的请求参数和要测试的接口发送给平台后端
2. (在调用前可以做一些调用)
3. 平台后端去调用模拟接口
1
2
//请求参数要注意大小写!
{"userName": "don"}

TODO

判断该接口是否可以调用时 由固定方法名改为根据测试地址来调用

用户测试接口 由固定方法名->根据测试地址来调用

模拟接口改为从数据库校验 akey

4、管理、统计分析

  1. 开发接口调用次数的统计

  2. 优化整个系统的架构

    a. 网关是什么?

    b. 网关的作用?

    c. 网关的应用场景及实现

    d. 结合业务去应用网关

接口调用次数统计

1、需求:用户每次调用接口成功,次数+1

​ 2、给用户分配或者用户自主申请接口调用次数

​ 业务流程:

​ 1、用户调用接口

​ 2、修改数据库,调用次数+1

5、鉴权

  1. 实现统一的用户鉴权、统一的接口调用次数统计(把API网关应用到项目中)
    • 用到的网关相关特性
      • 路由:用户原本直接请求模拟接口,在模拟接口鉴权;现在,我们在网关对用户进行鉴权,网关鉴权通过后再将请求重定向到模拟接口。
      • 统一鉴权(accessKey、secretKey)
      • 统一业务处理(每次请求接口后,调用次数都要+1)
      • 访问控制(黑白名单)
      • 流量染色(记录请求是否为网关来的,但是这样的话,在最终接口处还要对请求进行判断,看看有没有被染色)
      • 统一日志(记录每次的请求与响应)
    • 业务逻辑
      1. 用户发送请求到API网关
      2. 请求日志
      3. (黑白名单)
      4. 用户鉴权(判断ak、sk是否合法)
      5. 请求的模拟接口是否存在?
      6. 请求转发,调用模拟接口
      7. 响应日志
      8. 调用成功,接口调用次数+1
      9. 调用失败,返回一个规范的错误码

6、网关

  1. 补充完整网关的业务逻辑(怎么去操作数据库、怎么复用之前的方法——RPC)
  2. 完善系统,开发一个监控统计功能
网关业务逻辑

问题:网关项目比较纯净,没有操作数据库的包、并且还要调用我们之前写过的代码?复制粘贴维护麻烦

理想:直接请求到其他项目的方法

怎么调用其他项目的方法?:
  1. 复制代码、环境
  2. HTTP请求(提供一个接口、供其他项目使用)
    • server开发一个接口(地址、请求方法、参数、返回值)
    • client使用HTTP Client之类的代码包去发送HTTP请求
  3. RPC
    • 作用:像调用本地方法一样调用远程方法
    • 对开发者更透明,减少了很多沟通成本
    • RPC向远程服务器发送请求时,未必要使用HTTP协议,比如 TCP/IP(性能更高、内部服务更适用)等等等等
  4. 把公共项目打包成jar包,其他项目去引用(客户端SDK)

![image-20230719103013425](/Users/donn/Library/Application Support/typora-user-images/image-20230719103013425.png)

具体实现:
  1. 请求转发

用一个前缀匹配断言:所有路径为 /api/** 的请求转发到http://localhost:8123/api/**

例如:http://localhost:8090/api/name/** ===> http://localhost:8123/api/name/**

模拟接口请求地址:http://localhost:8123/api/name/get?name=123

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=source, donn
routes:
- id: api_route
uri: http://localhost:8123
predicates:
- Path=/api/**
  1. 编写业务逻辑

    使用了GlobalFilter全局请求拦截处理(类似AOP)

​ 因为网关项目没引入MyBatis等操作数据库的类库,如果该操作较为复杂,可以由backend增删改查项目提供接口,我们直接调用,不用再重复写逻辑。

  • HTTP请求 (用HTTPClient、用RestTemplate、Feign)
  • RPC (Dubbo)
问题

预期是等模拟接口调用完成,才调用响应日志、统计调用次数

但实际上 chain.filter 方法立即返回了,直到filter过滤器 return 后才调用了模拟接口

原因:chain.filter 是一个异步操作,理解为前端的 promise

解决方案:利用response装饰者,增强原有的response的处理能力

Dubbo框架(RPC实现)

​ 两种使用方式:

  1. Spring Boot 代码(注解 + 编程式):写java接口,服务提供者和消费者都去引用这个接口

  2. IDL (接口调用语言):创建一个公共的接口定义文件,服务提供者和消费者读取这个文件,优点是跨语言、所有的框架都认识

底层是Triple协议

整合运用:
  1. backend项目作为服务提供者,提供三个方法
  • 去数据库中查是否已分配给用户
  • 查询数据库,模拟接口是否存在,以及请求方法是否匹配
  • 调用成功后 调用次数+1 invokeCount
  1. Gataway项目作为服务调用者,调用这三个方法

7、完善网关、接口调用信息可视化、展望……

  1. 完成网关业务逻辑
    • 实际情况应该是去数据库查看是否已分配给用户(ak、sk是否合法)
      • 先根据accessKey判断用户是否存在,查到secretKey
      • 对比secretKey和用户传的加密后的secretKey是否一致
    • 从数据库查询模拟接口是否存在,以及请求方式是否匹配(还可以校验请求参数)
    • 调用成功后,调用次数+1 (invoke count
  2. 开发管理员分析的功能
  3. 上线
如何获取接口转发服务器的地址

​ 网关启动时, 获取所有的接口信息,维护到内存的hashmap中;有请求时,根据请求的url路径或者其他参数[比如host请求头]来判断应该转发到哪台服务器,以及用于校验接口是否存在

公共服务:

​ 目的是让方法、实体类在多个项目间复用,重复编写

1. 数据库中查询是否已分配给用户密钥(accessKey,返回用户信息,为空表示不存在)
1. 从数据库查询模拟接口是否存在(请求路径、请求方法、请求参数、返回接口信息,为空表示不存在)
1. 调用次数+1 (accessKey(标识用户)、接口路径)

步骤:

  1. 新建干净的maven项目,只保留必要的公共依赖
  2. 抽取service和实体类
  3. install本地 maven包
  4. 让服务提供方引入common包,测试是否正常运行
  5. 让服务消费方引入common包
扩展
  1. 怎么让其他用户也上传接口?

    需要提供一个机制(界面),让用户输入自己的接口host(服务器地址)、接口信息,将接口信息写入数据库。

    将接口信息写入数据库之前,要对接口进行校验,比如测试调用,保证接口正常。并遵循我们项目的要求(并且使用我们的sdk)

  2. 在interfaceInfo表中加入host字段,区分服务器地址,让接口提供者更灵活地接入系统。

  3. 网关判断是否还有调用次数

  4. 网关限流、提高性能等等

开发统计分析:

提供可视化平台,用图表的方式展示所有的调用情况,便于调整业务

需求:

​ 各接口总调用次数占比(饼图)前3的接口,从而分析出哪些接口没人用(降低资源或者下线)以及高频接口(增加资源、提高收费)

实现:
前端:

​ 展示饼图——推荐使用线程的库

​ ECharts、AntV

  • 看官网
  • 进入实例页面
  • 找到想要的图
  • 在线调试
  • 复制代码
  • 改为真实数据
后端:

​ 1. SQL查询调用数据

​ select interfaceInfoId, sum(totalNum) as totalNum from interface_info group by interfaceInfoId order by totalNum desc limit 3;

​ 2. 业务层去关联查询接口信息

上线:

前端:参考用户中心的上线方式

后端:

  • backend项目:web项目,部署springboot的jar包(对外的)
  • gateway项目:web项目,部署springboot的jar包(对外的)
  • interface 模拟接口项目:web项目,部署springboot的jar包(不建议对外暴露的)

如果自己学习用:单个服务器部署这三个项目足够

如果想搞大事:多个服务器建议在同一内网,内网交互会更快,且更安全

设计库表:

哪个用户?哪个接口?

用户 <==多对多==> 接口

因此需要再创建一个表:

用户调用接口关系表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 用户调用接口关系表
create table if not exists tjyapi.`user_interface_info`
(
`id` bigint not null auto_increment comment '主键' primary key,
`userId` bigint not null comment '调用用户id',
`interfaceInfoId` bigint not null comment '接口id',
`totalNum` int default 0 not null comment '总调用次数',
`leftNum` int default 0 not null comment '剩余调用次数',
`status` int default 0 not null comment '0-正常 1-禁用',
`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 '用户调用接口关系表';

步骤:

1. 开发基本增删改查(给管理员用)
1. 开发用户调用接口次数+1的功能(service)

​ 问题:

​ ——如果每个接口的方法都写调用次数+1,是不是比较麻烦?

​ ——致命问题:接口开发者需要自己去添加统计代码

使用AOP切面的优点:独立于接口,在每个接口调用后统计次数+1

AOP切面的缺点:存在于单个项目中,如果每个团队都要开发自己的接口,那么都需要写AOP切面—还是没有解决 问题

image-20230717140031212

网关相关知识:

​ 网关的优点:统一进行一些操作,处理一些问题

  • 路由

    • 起到转发的作用,比如有接口 A 和接口 B,网关会记录这些信息,根据用户访问的地址和参数,转发请求到对应的接口(服务器/集群)

      /a=>接口A

      /b=>接口B

  • 统一鉴权

    • 判断用户是否有权限进行操作,无论访问什么接口,都统一去判断,不用重复写
  • 统一跨域处理

    • 网关统一处理跨域
  • 缓存

  • 流量染色

    • 给请求(流量)添加一些标识,一般是设置在请求头中,添加新的请求头
  • 访问控制

    • 黑白名单,比如限制DDOS IP
  • 统一业务处理

    • 把每个项目中都要做的通用逻辑放到上层(网关),统一处理,比如本项目的接口调用次数统计
  • 发布控制

    • 灰度发布,比如上线新接口,先给新接口分配20%流量,慢慢增加,最终替代老接口
  • 负载均衡

    • 在路由的基础上

​ /c=>服务 A / 集群A(随机转发到其中的某一个机器)

  • 统一接口保护
    • 限制请求
    • 信息脱敏
    • 降级(熔断)
    • 限流
    • 超时时间
  • 统一日志
    • 统一的请求、响应信息记录
  • 统一文档
    • 将下游项目的文档进行聚合,在一个页面统一查看

网关的分类

  1. 全局网关(接入层网关):作用是负载均衡、请求日志等,不和业务逻辑绑定
  2. 业务网关(微服务网关):会有一些业务逻辑,作用是将请求转发到不同的业务/项目/接口/服务

实现:

  1. Nginx( 全局网关 )、Kong网关( API网关 Kong: https://github.com/Kong/kong ),编程成本高

  2. Spring Cloud Gateway (取代了 Zuul) 性能高,可以用Java代码来写逻辑,易于学习

参考文章:https://blog.csdn.net/qq_21040559/article/details/122961395

网关技术选型:https://zhuanlan.zhihu.com/p/500587132

Spring Cloud Gateway用法

官网:https://spring.io/projects/spring-cloud-gateway

核心概念

路由:根据什么条件,转发请求到哪里

断言:一组规则、条件,用来确定如何转发路由

过滤器:对请求进行一系列处理,比如添加请求头、添加请求参数

请求流程:
  1. 客户端发起请求
  2. Handler Mapping:根据断言,去将请求转发到对应的路由
  3. Web Handler:处理请求(一层层经过过滤器)
  4. 实际调用服务
两种配置方式:
  1. 配置式(application.yaml中配置) 方便、规范
  2. 编程式 相对麻烦、灵活
断言:(一些条件)
  1. after:在什么时间之后
  2. before……

……等等多种因素都能放进来考虑

建议开启日志——方便发现问题:

1
2
3
4
5
6
logging:
level:
org:
springframework:
cloud:
gateway: trace
过滤器:

基本(对请求头、响应头进行增删改查)

​ 在某设定路由下,做自定义的事(如添加请求头、添加请求体参数等等等等)

​ 或者 访问某路由如果失败,会转而访问另一个设定的路由(降级)

需求分析

  1. 管理员可以对接口信息进行增删改查
  2. 用户可以访问前台,查看接口信息

数据库表设计

接口信息表

id

name 接口名称

description 描述

url 接口地址

type 请求类型

requestHeader 请求头

responseHeader 响应头

status 接口状态 0-关闭 1-开启

isDelete

createTime

updateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use tjyapi;

-- 接口信息
create table if not exists tjyapi.`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 not null comment '是否删除 1-已删'
) comment '接口信息';

insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('唐果', '江胤祥', 'www.hui-russel.net', '7IE', 0, '姜烨磊', 7, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('范浩', '崔振家', 'www.peter-schmeler.info', 'Pj0', 0, '黄钰轩', 12857921, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('唐航', '孔健雄', 'www.tanner-bednar.name', 'Hi9QM', 0, '宋昊然', 18787, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('袁子默', '石明杰', 'www.mitsuko-zboncak.net', '4q', 0, '周涛', 954194648, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('朱博涛', '秦昊焱', 'www.rey-ondricka.biz', 'GhO', 0, '王瑾瑜', 43144, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('陆绍齐', '曹鸿涛', 'www.cathleen-gusikowski.org', 'pP0Z', 0, '尹乐驹', 23494, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('尹思聪', '贺钰轩', 'www.jeni-bartell.co', 'ZZdsp', 0, '冯果', 25646340, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('崔钰轩', '冯熠彤', 'www.timothy-legros.org', 'Qr', 0, '龙乐驹', 275486388, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('马子骞', '邹晋鹏', 'www.tori-wisoky.io', 'cwVv', 0, '熊俊驰', 9055718270, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('李嘉懿', '毛弘文', 'www.tawna-murazik.info', 'd8H', 0, '钟靖琪', 596652, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('龚绍齐', '崔伟祺', 'www.bart-dare.biz', 'VM', 0, '陆乐驹', 720475, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('严熠彤', '许炫明', 'www.lane-schoen.com', 'pc05M', 0, '余健雄', 481356844, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('邵凯瑞', '郭智渊', 'www.von-rempel.org', 'FSiqa', 0, '龙博超', 1368599, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('赖果', '杨天宇', 'www.kourtney-lubowitz.name', '3C', 0, '韩修杰', 6026675, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('段浩宇', '韩鸿涛', 'www.rupert-schmeler.co', 'mMo', 0, '魏远航', 76, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('沈炎彬', '张昊天', 'www.sheri-kerluke.co', 'adMn1', 0, '薛明', 3551, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('高黎昕', '谢志泽', 'www.sterling-adams.org', 'baP', 0, '金明哲', 91, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('胡语堂', '覃浩然', 'www.khalilah-breitenberg.biz', 'gDtv', 0, '雷睿渊', 21221843, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('陆雨泽', '曾雪松', 'www.jonas-fritsch.co', 'XvfU', 0, '侯智渊', 472525914, 0);
insert into tjyapi.`interface_info` (`name`, `description`, `url`, `responseHeader`, `status`, `method`, `userId`, `isDelete`) values ('邓昊焱', '秦胤祥', 'www.aron-buckridge.com', 'fBh3', 0, '丁子默', 12000, 0);

前端项目脚手架:

1
2
3
4
5
1. 在终端运行
npm i @ant-design/pro-cli -g
2. 进入你放的目录的终端,自动创建项目
pro create 项目名称

1
2
3
4
5
版本信息:
donn@Macc ~ % node -v
v16.13.0
donn@Macc ~ % npm -v
8.1.0

运行一下空项目:

image-20230711212835653

前端:ant design pro 脚手架

后端:直接使用模板 springboot-init

基础功能

增删改查、登录(复制粘贴)

前端接口调用:openapi插件自动生成

openapi规范

模拟接口项目 tjyapi-interface

提供三个模拟接口

  1. GET接口
  2. POST接口(url传参)
  3. POST接口(Restful)

调用接口

几种 HTTP 调用方式:

  1. HttpClient

  2. RestTemplate

  3. 第三方库(OKHTTP、Hutool)

Hutool:https://www.hutool.cn

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>

https://doc.hutool.cn/pages/HttpUtil/#概述

API签名认证

本质:
1. 签发签名
1. 使用签名(校验签名)
为什么需要?
  1. 保证安全性,不能随便来一个人都能调用
怎么实现?

通过http request header头 传递参数

参数1: accessKey:调用的标识 userA、userB (复杂、无序、无规律)

参数2: secretKey: 密钥——–该参数不传递到请求头中,通过私下约定进行同步

可以暂时理解为用户名和密码,区别为:ak、sk是无状态的;你每次访问都需携带正确的ak、sk才能访问

自行编写代码给每个用户生成ak、sk

千万不能把密钥直接在服务器之间传递,因为有可能被拦截

(所以要对密码进行进一步处理)

参数3: 用户请求参数

参数4: sign 参数

加密方式:对称加密、非对称加密、md5签名

用户参数:abc + 密钥 => 签名生成算法 => 不可解密的值

abc + miyao => dnasufbnusfuisdafiuadbfouiadbfuiasdbofibdaiksfbefewdf

服务端用一样的方式生成签名,只要结果和用户传来的一致,就表示一致

怎么防重放?

参数5: 每次请求时都 + nonce随机数,只能用一次

服务端要保存用过的随机数

参数6: 加timestamp时间戳,校验时间戳是否过期

API签名认证是一个很灵活的设计,具体有哪些参数,参数名如何 一定要根据具体场景来,(比如userId、appId、version、固定值等等)

思考:开发者每次调用接口都要自己写签名算法?

——开发一个简单易用的SDK

理想情况:开发者只需要关心调用哪些接口、传递哪些参数,就跟调用自己写的代码一样简单。

——开发一个starter的好处:

开发者引入后,可以直接在application.yaml中写配置,自动创建客户端

image-20230715142059798

spring-boot-configuration-processor的作用是自动生成配置的代码提示