yufan.me/source/_posts/2015-12-08.md
2024-06-14 02:15:18 +08:00

326 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Rest API 的那些事儿
tags:
- Restful
- API
categories: 学习
permalink: rest-api-desgin
featureImage: https://cat.yufan.me/cats/2015120801.gif
id: 144
updated: '2016-05-09 02:10:49'
date: 2015-12-08 03:13:21
---
# 一、前言
在软件行业快速发展的今天传统的软件授权已经不能足以满足一个IT类的公司的发展。虽然在大部分公司里它还是现金池的直接源头。但是在可遇见的未来受摩尔根理论的失效、物联网的发展等影响应用的架构会越来越趋于简单化架构越来越倾向于分布式水平扩展对外的服务提供也会越来越SaaS化。在这种大背景下很多公司都开始提供所谓的开放平台。
查阅各个大公司的开放平台我们不难发现都是Rest API都是HTTP请求响应报文都是大同小异的XML或者是JSON等众多雷同的特点。这是为什么呢让我们唠唠API平台的那些事。
<!--more-->
# 二、定义
[查看历史][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 业务系统架构变迁
![拆分前架构][7]
基本的软件公司的业务系统,一开始都是`单机,单节点,单库`。慢慢随着业务量的增加。这个系统越来越复杂机器性能越来越不能满足需求。于是第一种可能领导说换机器。上一台牛逼机器。但是机器性能有限越牛逼的机器价格越贵有时候都能买3~5台现有配置机器。于是就有了方案二三个臭皮匠赛过一个诸葛亮。`单应用,多机器,多节点`。
但是慢慢过了几年。你发现,这代码写的越来越屎,越来越复杂。基本上新来的开发要熟悉好几个月的业务。就代码因为快速上线。一堆坑,无法改。于是,大家现在都在做的事情,就是拆分。也就是现在常说的`SOA`。拆分,也有拆的好的,拆的不好的。不好的,就是一个大的恶心系统,变成了一堆恶心的小系统,互相调用,成一团乱码。小系统看似很好。但是某个不起眼的小系统。一挂,那么全部的系统都瘫了。这个时候这个万年不维护的小系统还找不到负责人,他么早就滚了。这就是拆分的技术欠债,你无法避免。
![服务化平台][9]
那么,就谈到拆分的架构设计。其实这块分两个架构,`技术架构和业务架构`。技术架构,就是要分清`技术系统和业务系统`。技术系统也可能是一个业务系统但它一定是一个通用的服务组件。它提供的服务无任何定制需求就是纯简单服务。比如发短信发邮件发微信的通知系统它就是通知。你业务有何特殊需求就在上面自己实现一个XXX通知系统那么业务系统的拆分才是最关键的。就是要**`划清边界`**。
>这个边界问题很可怕,什么你该做,什么你不该做。每个系统的职责都要明确。不要你也实现一个他也实现一个,然后相互调。**这种可怕性就是在两个服务都瘫痪的时候,完全都无法启用**。最后你的系统架构变成了一个通用技术组件系统,完成各个基础服务,每个产品线,业务端,基于你的技术系统包装出业务定制化服务系统,然后最上层就是业务子系统。业务子系统组合在一起,就是一个大的业务系统,也叫服务化平台。
**这个时候你需要做开放平台。暴露一套Restful API就是水到渠成了。**
PS实际的架构远比这个复杂截图选自《大型网站系统与Java中间件实践》。
### 2.2.2 基于不同系统架构的 API 平台
**1、演变**
![API平台架构演变][10]
初期的API平台往往是上图左侧那种某个庞大的业务系统希望暴露一套API于是大家就在这个系统上做直接设计一套协议。但是这样子带来的缺点十分明显第一它与业务联系太重理想的API平台是通用的不是只给你设计一个。第二它不好扩展每次变更都得和业务系统一同上线糟糕的情况下代码还会影响原有正常的业务。第三性能问题理论上会降低原有应用的性能。
这种情况下如果应用部署了多台机器多个节点我们就可以独立出来。也就是右边所示的API Gateway它做的事情本质上就是反向代理将外部的请求校验完合法性之后反代至内部实际想要对外暴露服务的服务集群上。
所以这种场景下API Gateway也就如名称所说就是一个入口。实际的Rest API的东西还是建立在各个业务子系统上只是只需要提供最简单的服务无需考虑授权等东西。用户管理API注册发布调用统计等均由API Gateway实现处理。对于想要快速上线的开发人员而言实在是一个不错的福音。
![SOA化的API架构][11]
然而当系统应用拆分到了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 交互流程
![简单交互图][12]
上面是一个简单的交互简单显示了外部系统和内部系统通过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&param2=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 PathRequest MethodRequest 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 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]
6. [Netflix 官网][6]
[1]: http://nirmata.com/2013/10/rest-apis-part-1/
[2]: http://nirmata.com/2013/11/rest-apis-part-2/
[3]: http://www.ruanyifeng.com/blog/2014/05/restful_api.html
[4]: http://www.ruanyifeng.com/blog/2011/09/restful.html
[5]: http://www.codeceo.com/article/rest-api.html
[6]: http://netflix.github.io/
[7]: https://cat.yufan.me/cats/20150120801.jpg
[9]: https://cat.yufan.me/cats/20150120803.jpg
[10]: https://cat.yufan.me/cats/20150120804.png
[11]: https://cat.yufan.me/cats/20150120805.png
[12]: https://cat.yufan.me/cats/20150120806.jpg