306 lines
20 KiB
Plaintext
306 lines
20 KiB
Plaintext
---
|
||
title: Rest API 的那些事儿
|
||
slug: rest-api-design
|
||
date: 2015-12-08 14:30:45
|
||
updated: 2020-09-10 06:26:16
|
||
category: 编程
|
||
cover: /images/2024/04/2024041520433700.jpg
|
||
summary: 在软件行业快速发展的今天,传统的软件授权已经不能足以满足一个IT类的公司的发展。虽然在大部分公司里,它还是现金池的直接源头。
|
||
---
|
||
|
||
![](/images/2024/04/2024041520451500.jpg)
|
||
|
||
# 一、前言
|
||
|
||
在软件行业快速发展的今天,传统的软件授权已经不能足以满足一个 IT 类的公司的发展。虽然在大部分公司里,它还是现金池的直接源头。但是在可遇见的未来,受摩尔根理论的失效、物联网的发展等影响,应用的架构会越来越趋于简单化,架构越来越倾向于分布式水平扩展,对外的服务提供也会越来越 SaaS 化。在这种大背景下,很多公司都开始提供所谓的开放平台。
|
||
|
||
查阅各个大公司的开放平台,我们不难发现,都是 Rest API,都是 HTTP 请求,响应报文都是大同小异的 XML 或者是 JSON 等众多雷同的特点。这是为什么呢?让我们唠唠 API 平台的那些事。
|
||
|
||
# 二、定义
|
||
|
||
[查看历史][4],我们惊讶地发现,其实 Rest 的概念早在 2000 年就被人提出。用一句话描述它,就是**用固定的 URI 和可变的参数访问某个服务,来完成一系列业务请求。**
|
||
|
||
1. 每一个 URI 代表一种资源;
|
||
2. 客户端和服务器之间,传递这种资源的某种表现层;
|
||
3. 客户端通过几个 HTTP 动词,对服务器端资源进行操作,实现“表现层状态转化”。
|
||
|
||
## 2.1 Rest API 格式
|
||
|
||
Rest API,无论它的名字多么高大上,它本质还是一个 HTTP 请求,POST 也好,GET 也罢,都是不同的数据提交方式。所以,能够决定一个 Rest API 的也就:URI、参数、请求方式、请求头等。
|
||
|
||
我们一般用 `URI` 来定义希望对外暴露的服务。结构基本类似 `schema://yourCompanyDomain/rest/{version}/{application}/{someService}`。`schema` 可以是 `http`,也可以是 `https`,`version` 指的是你这个API的版本,`application` 一般会指向底层的某个子系统,`someService` 就是这个子系统对外提供的服务。当然,如果按照业务为边界划分,也可将业务维度相同但隶属于底层不同的系统的服务定义为一个 `application`。
|
||
|
||
对于这种Rest请求,常见的响应结果就是XML或者是JSON形式,往往结果中会包含请求状态,和时间戳,业务系统响应结果。
|
||
|
||
具体的格式约定,可以看底部的参考文献。
|
||
|
||
### 2.1.1 API的版本概念
|
||
|
||
在 `URI` 的格式定义中,我们包含了version这个字段,这在早期,其实被认为是不优雅的方式。阮一峰有[一篇文章][4]就专门抨击这种设计,后面他又自己打脸说还是拼接version的好。_(不知道是不是因为Github的设计缘故)_
|
||
|
||
`API`设计时常会考虑版本这个概念,无论是在URI还是在请求参数里面,至少有一个地方得指明含版本,为什么呢?
|
||
|
||
这很简单,就和系统迭代一样,API也是快速迭代开发的。也许初期,你的老大脑袋一热,我们要上API,于是你们就加班加点做,设定了一版请求协议。当时你们想,写完就能赚钱,必然带来一堆问题,比如说,代码难以维护,功能单一。后面这个`API`的第二版,你们要基于它去做一些新的变更,比如请求参数多了,返回内容多了,必然就有了第二版。但是又不能影响现有的业务。这个`API`基本的`URI`没变,但是会同时存在两个版本,老用户继续请求旧版,没问题,你无需动旧版?有需求的新用户,你要请求新版才能提供服务。这样通过版本来区分了不同的服务,便于以后的升级和维护。_(就像BAE那货可以毫无压力地废弃掉2.0的API)_
|
||
|
||
**所以一开始设计API时,就要定义好版本。其次,版本化可以骗钱。** 我们可以满怀恶意地猜测,1.0为了抢用户,免费。2.0老牛逼了,你用的爽了,想更爽,付费。233333333
|
||
|
||
## 2.2 架构特点
|
||
|
||
API平台的架构,其实和底层公司的业务系统架构有着密切的关系,基于一个好的系统架构写一个Rest API平台基本是水到渠成的。我们先从业务系统的架构变迁说起,再来分析上面的API平台。
|
||
|
||
### 2.2.1 业务系统架构变迁
|
||
|
||
基本的软件公司的业务系统,一开始都是`单机,单节点,单库`。慢慢随着业务量的增加。这个系统越来越复杂,机器性能越来越不能满足需求。于是,第一种可能,领导说,换机器。上一台牛逼机器。但是机器性能有限,越牛逼的机器价格越贵,有时候都能买3~5台现有配置机器。于是就有了方案二,三个臭皮匠赛过一个诸葛亮。`单应用,多机器,多节点`。
|
||
|
||
但是慢慢过了几年。你发现,这代码写的越来越屎,越来越复杂。基本上新来的开发要熟悉好几个月的业务。就代码因为快速上线。一堆坑,无法改。于是,大家现在都在做的事情,就是拆分。也就是现在常说的`SOA`。拆分,也有拆的好的,拆的不好的。不好的,就是一个大的恶心系统,变成了一堆恶心的小系统,互相调用,成一团乱码。小系统看似很好。但是某个不起眼的小系统。一挂,那么全部的系统都瘫了。这个时候这个万年不维护的小系统还找不到负责人,他么早就滚了。这就是拆分的技术欠债,你无法避免。
|
||
|
||
那么,就谈到拆分的架构设计。其实这块分两个架构,`技术架构和业务架构`。技术架构,就是要分清`技术系统和业务系统`。技术系统也可能是一个业务系统,但它一定是一个通用的服务组件。它提供的服务无任何定制需求,就是纯简单服务。比如,发短信发邮件发微信的通知系统,它就是通知。你业务有何特殊需求,就在上面自己实现一个XXX通知系统,那么业务系统的拆分,才是最关键的。就是要**`划清边界`**。
|
||
|
||
>这个边界问题很可怕,什么你该做,什么你不该做。每个系统的职责都要明确。不要你也实现一个他也实现一个,然后相互调。**这种可怕性就是在两个服务都瘫痪的时候,完全都无法启用**。最后你的系统架构变成了一个通用技术组件系统,完成各个基础服务,每个产品线,业务端,基于你的技术系统包装出业务定制化服务系统,然后最上层就是业务子系统。业务子系统组合在一起,就是一个大的业务系统,也叫服务化平台。
|
||
|
||
**这个时候,你需要做开放平台。暴露一套Restful API,就是水到渠成了。**
|
||
|
||
PS,实际的架构远比这个复杂,截图选自《大型网站系统与Java中间件实践》。
|
||
|
||
### 2.2.2 基于不同系统架构的 API 平台
|
||
|
||
**1、演变**
|
||
|
||
初期的API平台往往是上图左侧那种,某个庞大的业务系统希望暴露一套API,于是大家就在这个系统上做,直接设计一套协议。但是,这样子带来的缺点十分明显,第一,它与业务联系太重,理想的API平台是通用的,不是只给你设计一个。第二,它不好扩展,每次变更都得和业务系统一同上线,糟糕的情况下代码还会影响原有正常的业务。第三,性能问题,理论上会降低原有应用的性能。
|
||
|
||
这种情况下,如果应用部署了多台机器,多个节点,我们就可以独立出来。也就是右边所示的API Gateway,它做的事情本质上就是反向代理,将外部的请求校验完合法性之后反代至内部实际想要对外暴露服务的服务集群上。
|
||
|
||
所以,这种场景下,API Gateway也就如名称所说,就是一个入口。实际的Rest API的东西还是建立在各个业务子系统上,只是只需要提供最简单的服务,无需考虑授权等东西。用户管理,API注册发布,调用统计等,均由API Gateway实现处理。对于想要快速上线的开发人员而言,实在是一个不错的福音。
|
||
|
||
然而,当系统应用拆分到了SOA化之后,API的架构由有了新的变革,我们有了注册中心的概念。因为SOA化,所以每个业务子系统其实都有了对外的统一接口,有了ESB(注册中心)。实际的内部系统间请求也有了较好的路由、熔断等策略。
|
||
|
||
在这种大背景下,API平台对外暴露的Rest API无需底层的业务专门开发了。直接使用现有的内部接口,选择性暴露即可。问题点就在于,如何根据定义的Rest API请求,实际模拟内部的RPC协议请求。
|
||
|
||
某种程度上,这时候的API平台,已经不仅仅是HTTP Rest请求了。我们完全可以实现相同RPC协议的透传,比如你就是一个Hessian接口想对外暴露,我只需包上一层认证,直接注册于API Gateway,外部Hessian请求直接透传至内部子系统。
|
||
|
||
在这个基础上的 Rest API 平台,才是灵活的,可扩展的,易于维护的。然而有得必有失,Mock请求必然会有性能上的损耗,但是这个架构的公司,已经不在乎钱了,上10台虚机,不够加呗。
|
||
|
||
**2、特点**
|
||
|
||
1. C/S结构
|
||
2. 无状态(API平台无需存储业务状态,只做认证和转发)
|
||
3. 有缓存,API会对指定URI的请求转发做缓存,保证并发性,业务系统也对同样的请求针对性缓存。
|
||
4. 结构分层,每层间无法直接访问。
|
||
|
||
API平台的背后,就是庞大的各个业务子系统。每个API,就相当于一个业务子系统。API平台要做的事,就非常清晰和简单。就是业务子系统注册发布API,对外部请求校验计费,模拟请求内部业务子系统,对子系统结果包装序列化为`JSON`返回。
|
||
|
||
## 2.3 交互流程
|
||
|
||
上面是一个简单的交互,简单显示了外部系统和内部系统通过Rest API的交互过程:开发者(企业)注册,申请APP_KEY,开通API。按照开发接入,请求签名。转发至后端调用返回结果,API平台计费(预付费或者后收费),统计调用情况。
|
||
|
||
外部通信本质上还是`HTTP`,那么必然存在了授权问题,生产的API平台是直接暴露于公网的,如果认证授权策略出现纰漏,影响是可怕的。
|
||
|
||
比如,这个API是群发短信,你要是没有好的授权体系,允许人随意推送。某个人想搞你,调用发布反共信息,你整个公司都会跨。`HTTP`协议本质上没有这一块的内容,所以我们必然要在这上面考虑安全策略的内容。
|
||
|
||
### 2.3.1 如何保证Rest API的安全性
|
||
|
||
如果单纯考虑加解密,或者签名方式来保证请求合法,其实是远远不够的。事实上,一个安全的API平台往往需要多方面一起考虑,保证请求安全合法。
|
||
|
||
**1、是不是实际客户端的请求?**
|
||
|
||
1. 设计专门的私有请求头:定义独有的Request headers,标明有此请求头的请求合法。
|
||
2. 请求包含请求时间:定义时间,防止中间拦截篡改,只对指定超时范围内(如10秒)的请求予以响应。
|
||
3. 请求URI是否合法:此URI是否在API平台注册?防止伪造URI攻击
|
||
4. 请求是否包含不允许的参数定义:请求此版本的这个URI是否允许某些字段,防止注入工具。
|
||
5. 部分竞争资源是否包含调用时版本(Etag):部分竞争资源,使用If-Match头提供。如用户资金账户查询API,可以返回此时的账户版本,修改扣款时附加版本号(类似乐观锁设计)。
|
||
|
||
**2、API平台是否允许你调用(访问控制)?**
|
||
|
||
访问控制,主要是授权调用部分。API都对外暴露,但是某些公共API可以直接请求,某些,需要授权请求。本质的目的,都是为了验证发起用户合法,且对用户能标识统计计费。
|
||
|
||
以HMac Auth为例,我们简单设计一个签名算法。开发者注册时获取App Key、App Secret,然后申请部分API的访问权限,发起请求时:
|
||
|
||
1. 所有请求参数按第一个字符升序排序(先字母后数字),如第一个相同,则看第二个,依次顺延。
|
||
2. 按请求参数名及参数值相互连接组成一个字符串。param1=value1¶m2=value2...(其中包含App Key参数)
|
||
3. 将应用密钥分别添加到以上请求参数串的头部和尾部:secret + 请求参数字符串 + secret。
|
||
4. 对该字符串进行 SHA1 运算,得到一个二进制数组。
|
||
5. 将该二进制数组转换为十六进制的字符串,该字符串为此次请求的签名。
|
||
6. 该签名值使用sign系统级参数一起和其它请求参数一起发送给API平台。
|
||
|
||
服务端先验证`是不是实际客户端的请求`,然后按照App Key查找对应App Secret,执行签名算法,比较签名是否一致。签名一致后查看此App Key对应的用户是否有访问此API的权限,有则放行。
|
||
|
||
执行成功后包装返回指定格式的结果,进行统计计费。
|
||
|
||
# 三、需求与实现
|
||
|
||
## 3.1 需求
|
||
|
||
### 3.1.1 系统需求
|
||
|
||
1. 支持rest类API接口动态发布及运营,包括但不限于:
|
||
* 安全认证
|
||
* 会话管理
|
||
* 流量统计及限流
|
||
* 计费收费
|
||
* 熔断
|
||
2. 支持现有子系统RPC协议的API动态发布及运营,外部请求透传。
|
||
3. 支持json、xml响应报文,可以请求时选取所需报文格式。
|
||
4. 支持动态直接将后端SOA服务暴露为API。
|
||
5. 支持动态将普通Web接口暴露为API。
|
||
6. 支持动态将MQ服务暴露为API。
|
||
7. 支持多个服务组合编排后暴露为API。
|
||
|
||
### 3.1.2 业务需求
|
||
|
||
**1、API管理**
|
||
|
||
所有API可后台查询管理,包括动态发布、参数映射配置、后端服务接口配置、API禁用、启用,多版本、分组、分级别等。
|
||
|
||
**2、应用管理**
|
||
|
||
后台管理开放平台接入的应用(第三方应用),包括查询、禁用、启用、审核。
|
||
|
||
**3、API鉴权&授权**
|
||
|
||
1. 应用申请审核通过后生成公钥,开放平台需提供支持分布式系统的密钥管理
|
||
2. 服务可设置为两个安全等级:需授权访问和无需授权访问(后者即任意客户端都可以发起调用),默认所有API都需授权访问。
|
||
3. 非正常状态(禁用、停用、黑名单等)的应用直接抛异常不允许访问——**熔断机制**
|
||
* 调用次数、调用频率、并发数可运行时控制,避免某请求量过大影响其他应用的调用。
|
||
* 可对某个应用某个API设置强制熔断,所有请求无视阀值直接抛出异常。
|
||
4. 易用性
|
||
* 与SOA集成,SOA服务一键发布到API平台。
|
||
* 支持后台动态发布API,而不是新上一个API就需上线一次。
|
||
|
||
**4、计费统计**
|
||
|
||
1. API的调用统计,每笔请求时间,响应时间,响应状态。
|
||
2. API的计费计算,按照请求量和请求资源计费,实现多种计费模型。(预付费,后收费。按量,按时间周期。)
|
||
|
||
**5、开发者平台**
|
||
|
||
1. API开发者平台,开发者注册、访问、申请API授权、计费统计、调用统计。
|
||
2. API文档系统,详细的API文档展示,SDK下载,用户登录后还可专门生成不同编程语言请求,在线模拟请求结果等。
|
||
|
||
### 3.1.2 角色定义
|
||
|
||
**1、外部用户**
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>用户</th>
|
||
<th>做什么</th>
|
||
<th>使用目的</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>API平台接入方</td>
|
||
<td>接入API平台</td>
|
||
<td>使用XXXX提供的开放平台服务</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
**2、各个业务产品线**
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>用户</th>
|
||
<th>做什么</th>
|
||
<th>使用目的</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>各个业务产品线</td>
|
||
<td>作为外部应用接入API平台</td>
|
||
<td>使用XXXX提供的开放平台服务</td>
|
||
</tr>
|
||
<tr>
|
||
<td>各个业务产品线</td>
|
||
<td>提供服务</td>
|
||
<td>提供后端服务,发布到API平台供外部应用接入</td>
|
||
</tr>
|
||
<tr>
|
||
<td>公司后端应用</td>
|
||
<td>提供服务</td>
|
||
<td>提供后端服务,发布到API平台供外部应用接入</td>
|
||
</tr>
|
||
<tr>
|
||
<td>API平台</td>
|
||
<td>API治理</td>
|
||
<td>运营,管理API、第三方应用等</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
## 3.2 请求模型
|
||
|
||
API 的所有服务请求域名是相同的,区别在于Request Path等。请求参数分为系统级参数和业务级参数两部分,系统级参数是所有 API 都拥有的参数,而业务级参数由具体服务 API 定义。
|
||
|
||
### 3.2.1 统一服务 URL
|
||
|
||
建立API Gateway接受所有请求,按照Request Path,Request Method,Request Head分发所有的请求。
|
||
|
||
**1、 通用统一URL**
|
||
|
||
**格式**:`schema://<api Gateway URI>/DispatcherServlet?method=XXService.xxMethod?xxxObj.xxxParam=xxxValue。`
|
||
|
||
**说明**:所有请求直接走`DispatcherServlet`分发,所有内容均定义于URL参数中。`method`为后端某个子系统的某个方法。`xxxObj.xxxParam`为方法参数实体的某个属性的值定义。
|
||
|
||
**示例**:
|
||
`http://api.xxxxx.com/router?method=SMSService.sendSMS&user.phoneNumber=18888888888&sign=ds234324sdsad&date=20151229231232`
|
||
|
||
**2、Rest类型URL**
|
||
|
||
**格式**:`schema://</api><api Gateway URI>/rest/{version}/{service}/{method}/{params}`
|
||
|
||
**说明**:请求按照Gateway定义的Rest地址匹配,动态映射至具体系统具体方法,模拟调用。请求中包含`version`字段。
|
||
|
||
**示例**:
|
||
`http://api.xxxx.com/rest/v1/XXService/xxMethod/{xxParam}`
|
||
`http://api.xxxx.com/rest/v1/XXService/xxMethod?xxxParam=xxxValue`
|
||
|
||
### 3.2.2 参数设计
|
||
|
||
**1、系统级参数**
|
||
|
||
系统级参数是由 API 平台定义的一组参数,每个服务都拥有这些参数,用以传送框架级的参数信息。如我们前面提到的 method 就是一个系统级参数,使用该参数指定服务的名称。
|
||
|
||
**2、业务级参数**
|
||
|
||
业务级参数,顾名思义是由业务逻辑需要自行定义的,每个服务 API 都可以定义若干个自己的业务级参数。API Getaway 根据参数名和请求属性名相等的契约,将业务级参数具体的方法请求对象中。
|
||
|
||
## 3.3 常见框架
|
||
|
||
1. Kong:[https://github.com/Mashape/kong](https://github.com/Mashape/kong)
|
||
2. Zuul:[https://github.com/Netflix/zuul](https://github.com/Netflix/zuul)
|
||
3. ROP:[https://github.com/itstamen/rop](https://github.com/itstamen/rop)
|
||
4. Resty: [https://github.com/Dreampie/Resty](https://github.com/Dreampie/Resty)
|
||
# 四、优劣
|
||
|
||
## 4.1 好处
|
||
|
||
1. **跨平台**,管你是`Java`,还是`PHP`,还是`Node.js`还是`Go`,你丫都得支持`HTTP`请求。我`API`平台只需要提供这个语言的`SDK`,保证能按照消息协议调用就好。
|
||
|
||
2. **将复杂的内部业务系统抽象为通用调用请求**。包装了复杂的业务逻辑,对外提供统一的,好管理的接口。并可以定制化设计,计费,授权一类的容易管理。
|
||
|
||
## 4.2 坏处
|
||
|
||
1. **协议描述能力弱化**,`Restful`的`URI`无法完全对请求参数做强格式校验。最后的方法参数绑定,模拟内部请求时往往容易出问题,尤其是以Java等强格式语言的系统。不能像`WebService`一样清晰描述请求报文。
|
||
|
||
2. 同样的道理,响应结果为了是`JSON`、`XML`。这当中,编码,正反序列化,等操作,往往就会有性能瓶颈。而且,Java在这块资源消耗极大。以Github的`ROP`这个框架为例,当年测试时,它在并发请求过高的时候就会有一个内存泄漏问题。
|
||
|
||
----
|
||
|
||
# 附:参考资料
|
||
|
||
1. [REST Is Not About APIs, Part 1][1]
|
||
2. [REST Is Not About APIs, Part 2][2]
|
||
3. [RESTful API 设计指南][3]
|
||
4. [理解RESTful架构][4]
|
||
5. [撰写合格的REST API][5]
|
||
|
||
[1]: https://nirmata.com/2013/10/01/rest-apis-part-1/
|
||
[2]: https://nirmata.com/2013/11/12/rest-apis-part-2/
|
||
[3]: https://www.ruanyifeng.com/blog/2014/05/restful_api.html
|
||
[4]: https://www.ruanyifeng.com/blog/2011/09/restful.html
|
||
[5]: https://kb.cnblogs.com/page/521718/
|