Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/owenliang/web-api-the-good-parts
《Web API的设计与开发》--知识点整理
https://github.com/owenliang/web-api-the-good-parts
api web
Last synced: 19 days ago
JSON representation
《Web API的设计与开发》--知识点整理
- Host: GitHub
- URL: https://github.com/owenliang/web-api-the-good-parts
- Owner: owenliang
- Created: 2018-05-04T05:32:04.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2020-10-02T10:52:56.000Z (over 4 years ago)
- Last Synced: 2024-11-22T20:36:58.670Z (3 months ago)
- Topics: api, web
- Homepage:
- Size: 31.3 KB
- Stars: 88
- Watchers: 5
- Forks: 25
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Web API的设计与开发
## 说明
丰富了附录B中的Web API检查清单,给予了一些必要的解释与说明,方便大家快速学习。
另外,以下只是作者的建议,使用需结合实际情况。
## 检查清单
* URI是否短小且容易输入
```
好的例子:http://api.example.com/search
坏的例子:http://api.example.com/service/api/search。域名已经是api,在URI就没有必要重复一些毫无意义的单词。
```* URI是否能让人一眼看懂
```
不要轻易使用缩写:http://api.example.com/sv/u
使用更地道的英语表达,比如搜索接口一般用search而不是find,可以多参照一些国外大厂的API。
```* URI是否只有小写字母组成
```
HTTP协议指定了:URL中除了schema和hostname不区分大小写,其他部分均大小写敏感。
因此,URI应该使用小写,禁止大小写混写。
```* URI是否容易修改
```
比如获取某个商品信息的URI应该长这样:http://api.example.com/v1/items/12346
从URI直观的即可知道获取56780商品的URI是这样:http://api.example.com/v1/items/56780。开发者对URI的行为理解是自然的,不费心的。
```* URI是否反映了服务端的架构
```
不要暴露服务端是哪种开发语言,下面是错误的例子:
http://api.example.com/cgi-bin/get_user.php?user=100
```* URI规则是否统一
```
URI中的词汇和结构应该保持统一。下面是一个错误的例子:
获取好友信息:http://api.example.com/friends?id=100
发送信息:http://api.example.com/friend/100/messages首先,friends和friend的单复数形式不统一,你可以想象API使用者是如何被这种细小的差异坑了半天。
其次,一个通过get参数传参,一个通过URI路径传参,不够统一。一个正确的例子:
获取好友信息:http://api.example.com/friends/100
发送信息:http://api.example.com/friends/100/messages该例子遵循REST风格,下面讲解REST URI设计风格。
```
* 有没有使用合适的HTTP方法
```
作者强调REST风格。HTTP方法表示"进行怎样的操作",URI表示"资源",HTTP和URI一起则表示"对什么资源做什么操作"。
GET:获取资源
获取ID=100的好友信息,
GET http://api.example.com/friends/100POST:新增资源
添加一位好友,相当于新建一个好友关系:
POST http://api.example.com/friendsPUT:更新已有资源
更新ID=100的好友信息(例如:更新备注信息),
PUT http://api.example.com/friends/100PATCH:更新部分资源
和PUT类似,只是强调更新资源的部分信息,不常用。
DELETE:删除资源
删除ID=100的好友信息:
DELETE http://api.example.com/friends/100HEAD:获取资源的元信息
这个作者没有详细去说一个资源的元信息应该是什么样的。
感觉和自己设计的API不太一样吧?这就是REST风格,常见于国外各种大厂。
```* URI里用到的单词所表达的意思是否和大部分API相同
```
有点重复,还是强调对于非英语母语的开发者,应该注意使用地道准确的单词。
比如,照片应该用photo而不是picture。
```* URI里用到的名词是否采用了复数形式
```
因为URI表示资源的集合,所以作者是建议总是使用复数形式。正确的例子:http://api.example.com/friends/100
错误的例子:http://api.example.com/friend/100另外,因为REST风格强调URI是资源,所以不应该在URI里出现动词,因为动作是HTTP方法表达的。
错误的例子:http://api.example.com/get_friend?id=100
```* URI里有没有空格符以及需要编码的字符
```
URL是会被urlencode编码的,所以不要在URI里使用空格(会被编码成+)、UTF-8字符、乱七八糟的符号等。即不要影响URI的可读性。
```* URI里的单词和单词之间有没有使用连接符
```
因为URL中的hostname不允许使用下划线,所以作者建议URI部分总是使用连字符-来连接多个单词。
```* 分页的设计是否恰当
```
分页参数分2种风格,可以按情况使用:第一种,表示第3页,每一页50条:
page=3&per_page=50第二种:表示从100条开始的50条:
offset=100&limit=50前者page从1开始增长,后者offset从0开始增长。
前者对用户来说自由度较低,缓存命中率较高。
后者对用户来说自由度较高,缓存命中率较低。上述翻页风格称为"相对位置",深度翻页性能差(可以自己扩展学习),如果数据集合有更新,则翻页时可能看到重复内容或者错过一些内容。
与之相对的是"绝对位置",即指定last_id之前的N条数据,下次使用新的last_id继续获取。(个人理解瀑布流页面比较适合)
```
* 登录有没有使用OAuth2.0
```
很常见的认证规范,让用户通过大厂的账号系统登录,并授权给第三方获取用户信息的权限。作者表达的意思是你如果对外提供API,应该提供OAuth2.0认证,这样第三方调用API时携带access_token,我们即可校验其是否有权使用API。
最后,作者描述了一下REST API的几个等级:
REST LEVEL0:使用HTTP
REST LEVEL1: 引入资源的概念
REST LEVEL2:引入HTTP动词(GET/POST/PUT/DELETE等)
REST LEVEL3:引入HATEOAS概念本书是REST LEVEL2。
LEVEL3中的HATEOAS概念尚未普及,其思路是API返回的数据中应该包括下一步行为对应的URI是什么,客户端请求下一步的URI就可以得到进一步的数据。
比如文章列表的返回值:
{
"articles": [
{
"title": "good",
"uri": "https://api.example.com/v1/articles/12345",
"rel": "article/detail"
}
]}
每一篇文章的uri告知客户端如何获取文章详情,属于一种高度灵活。
```
* 响应数据格式有没有使用JSON作为默认格式
```
越简单的东西越容易普及,JSON比XML简单的多,也满足需求,另外Javascript天然支持json。客户端没有明确指定返回值格式的话,JSON应该作为默认的返回值格式。
```* 是否支持通过查询参数来指定数据格式
```
如果服务端支持多种返回数据格式,那么客户端可以指定。通过get参数:
https://api.example.com/v1/users?format=xml通过扩展名:
https://api.example.com/v1/users.xml通过HTTP头部:
GET /v1/users
Host: api.example.com
Accept: application/xml作者建议首先使用HTTP头部,因为更符合HTTP协议规范;其次使用查询参数,避免使用扩展名。
```* 是否支持不必要的JSONP
```
JSONP可以实现跨域HTTP调用,其原理是基于加载一段服务端的javascript代码(自行扩展学习)。API如果决定支持JSONP,可以在服务端判定客户端是否上传了callback参数,如果上传了就返回jsonp格式。
客户端为了区分不同的JSONP调用,需要为每个JSONP调用生成一个唯一的全局回调函数名,这一点Jquery可以帮我们实现。
出于正确性与安全性考虑,JSONP服务端返回时应该设置Content-Type: application/javascript而不是application/json,因为返回的是一段js函数调用代码。
作者提到,JSONP因为是作为一个script引入的,服务端可以通过2种方式返回错误信息:
1)在json响应体里放置error信息
2)在callback之外支持error_callback传参
```* 响应数据的内容能不能从客户端指定
```
有时调用方只需要部分信息,比如:用户信息接口只希望获取用户ID,这样可以节约通讯量。此类接口可以通过类似fields的get参数来指定返回哪些信息:
http://api.example.com/v1/users/12345?fields=name,age另外也可以提前准备几种返回值的组合,称为响应群(response group),比如:
http://api.example.com/v1/users/12345?group=basic_info
其中,basic_info表示返回用户的基础信息,例如name和age。
```* 响应数据中是否存在不必要的封装
```
作者出于rest风格原因,建议把错误码和错误信息放在http header里,而不是放在body里。一个错误的例子:
HTTP/1.1 200 OK
{
"error_code": 500,
"error_msg": "参数错误",
"data": {}
}
作者认为HTTP返回200,但内容却表达了500失败,这样很奇怪,很不rest。rest风格更建议用下面这种方式:
HTTP/1.1 500 参数错误
{
"data": {}
}
```* 响应数据的结构有没有尽量做到扁平化
```
不要在JSON中增加无意义的多余层级,尽可能扁平化。一个错误的例子:
{
"id": 12345,
"name": "hahaha",
"profile": {
"birthday": "0203",
"gender": "male",
"language": ["zh", "en"]
}
}增加profile并没有带来什么价值,不如扁平化:
{
"id": 12345,
"name": "hahaha",
"birthday": "0203",
"gender": "male",
"language": ["zh", "en"]
}不仅访问起来方便,而且传输的内容也少了。
```
* 响应数据有没有用对象来描述,而不是用数组```
作者建议JSON返回值总是使用{}作为返回值的最外层,而不要直接返回数组[]。正确的例子:
{
"articles": [
{"id": 1},
{"id": 2}
...
]
}
错误的例子:
[
{"id": 1},
{"id", 2}
]这样做有2个次要的理由:
1,因为从字面看,articles能直接表达数据的含义
2,客户端在处理JSON应答时,可以统一将最外层作为对象去解析,不需要为数组做适配。
3,有一些安全性考虑,后面会提到。```
* 响应数据的名称所选用的单词的意思是否和大部分API相同
```
对非英语母语的人,多模仿大厂使用的常见单词。
```* 响应数据的名称有没有用尽可能少的单词来描述
```
关于用户注册时间字段,错误的例子:userRegistraionDataTime
这个单词太长了,很容易打错,也不容易记忆。正确的例子:registeredAt
```* 响应数据的名称由多个单词连接而成时,连接方法在整个API里是否一致
```
有几种连接单词的方法:
1,user_id:蛇形法
2,user-id:脊柱法
3,userId:驼峰法在JSON和Javascript中,都是建议使用驼峰法的,但是保持风格一致是最重要的。
```* 响应数据的名称有没有使用奇怪的缩写形式
```
尽量避免奇怪的缩写,比如timezone写成tz。如果出于数据量大小的考虑而采用缩写,属于特殊情况。
```* 响应数据的名称的单复数形式是否和数据内容相一致
```
只为数组采用复数,比如friends。
其他情况使用单数。一致的命名风格,API使用者会顺其自然,养成习惯。
```* 出错时响应数据中是否包含有助于客户端剖析原因的信息
```
出错时,响应信息应该包含2部分:
1,错误码
2,错误原因作者建议使用http header返回错误码,每种错误码的含义如下:
1xx:消息
2xx:成功
3xx:重定向
4xx:客户端原因引起的错误
5xx:服务端原因引起的错误而不是在JSON中设置一个error_code字段,因为http code本身就是这个意思。
当http code为4xx或者5xx时,需要进一步告知客户端错误原因,这时候有2种做法:
1,在http header里自定义一些头部信息,保存错误原因,例如:
X-ERROR-MESSAGE: params error
2,在body中返回JSON格式的错误信息:
{
"errors": [
{
"message": "参数fields错误"
},
{
"message": "参数last_id错误"
}
]
}
作者建议采用第2种方式,因为可以描述多个错误原因,并且很多大厂都是这么做的。```
* 出错时有没有返回HTML数据
```
当服务端发生错误时,很多web框架会打印一个html错误信息页面。对于API来说,当发生错误时也应该返回一个合法的JSON结构,因为客户端假设服务端返回JSON,返回HTML可能导致异常。
```* 有没有返回合适的状态码
```
这一条规则有点重复,主要是指返回适当的http code。
```* 服务器端在维护时有没有返回503状态码
```
当服务器需要停机维护时,按照Google爬虫的建议,应该返回503错误码,并且在header中告知维护的结束时间:503 Service Temporarily Unavailable
Retry-After: Mon, 2 Dec 2013 03:00:00 GMT这遵循HTTP1.1规范,客户端需要实现逻辑去识别这个情况,但是至少google爬虫会去理解这些信息。
```* 有没有返回合适的媒体类型
```
有的HTTP客户端会校验应答中的Content-Type字段,因此服务端如果返回的是JSON,那么就应该返回Content-Type: application/json而不是Content-Type: text/html,这样避免一些严格的客户端出现解析失败。
```* 必要时能不能支持CORS
```
浏览器有同源策略,禁止跨域Ajax请求。API可以支持CORS跨域资源共享,比如http://www.example.com请求http://api.example.com的API时应该携带请求的来源:
Origin: http://www.example.com服务端只允许某些来源的跨域调用,如果Origin合法就在返回中携带:
Access-Control-Allow-Origin: http://www.example.com
或者
Access-Control-Allow-Origin: *浏览器看到这样的应答,就会把ajax请求正常执行完成,否则会报告ajax调用失败。
对于一些特殊场景,浏览器会采用"事先请求"的方式,先通过一个OPTION方法调用到对应的接口来试探服务端是否返回Access-Control-Allow-Origin,如果没有返回则不发起真正的数据请求。
CORS客户端默认不会传输cookie,我们在发起ajax前设置XHTTPRequest.withCredentials=true,并且服务端必须返回header:Access-Control-Allow-Credentials: true,否则这次ajax调用将报告失败。
```* 有没有返回Cache-Control、ETag、Last-Modified、Vary等首部以便客户端采用合适的缓存策略
```
缓存模型分2种:
1,过期模型:Expires、Cache-Control
2,验证模型:Last-Modified、ETag过期模型是指,浏览器在过期之间直接使用本地缓存文件,下面是一个例子:
Expires: Fri, 01 Jan 2016 00:00:00 GMT
Cache-Control: max-age=3600Cache-Control是HTTP1.1协议出现的,Expires是HTTP1.0,前者优先级更高。
并且HTTP1.1协议也规定,缓存时间不应超过1年,但实际上客户端可能没有遵循这个约束。验证模型是指,客户端照常发起请求,但在header中携带附加条件,服务器根据附加条件判断若数据没有修改则返回304,客户端直接使用本地缓存即可,否则返回200并携带内容。
下面是个例子,
请求:
GET /v1/users/12345
If-Modified-Since: Tue, 01 Jul 2014 00:00:00 GMT
If-None-Match: "ff39b31e285573ee373af0d492aca581"
应答:
HTTP/1.1 304 Not Modified
Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT
ETag: "ff39b31e285573ee373af0d492aca581"需要注意ETag分为强验证和弱验证:
强验证是指资源的真实内容完全不能变,弱验证是指逻辑上资源没有改变即可。
```* 不想缓存的数据有没有添加Cache-Control: no-cache首部信息
```
如果不希望被客户端缓存,可以指定Cache-Control: no-cache。如果你的API前面存在反向代理缓存,可以额外声明Cache-Control: no-store,这样代理服务器也不会缓存数据了。
客户端可能多次请求同一个API,但是请求的http header不同,导致返回的内容结构不同,比如:
客户端携带 Accept: application/json,则服务端返回的是JSON。
客户端携带 Accept: application/xml,则服务端返回的是XML。如果反向代理根据URI缓存,则会导致无法根据客户端的要求返回正确格式,此时我们API应该在返回值里携带Vary: Accept,这样缓存服务器会为不同的Accept分别缓存。
```* 有没有对API进行版本管理
```
一般API会不断的迭代功能,有时会出现无法向下兼容的情况。通常老客户端会依旧使用老版本的API,新客户端使用新版本的API,并在合适的时机完全下线掉老版本的API。
```* API版本的命名有没有遵循语义化版本控制规范
```
作者介绍了语义化版本控制,通常版本号是a.b.c这样的,分别表示主版本号,次版本号,补丁版本号。1,如果软件API没有变更,只是修复服务端BUG,那么就增加补丁版本号
2,对软件API实施了向下兼容的变更,增加次版本号
3,对软件API实施了不向下兼容的变更时,增加主版本号```
* 有没有在URI里嵌入主版本编号,并且能够让人一目了然
```
对于Web API来说,作者建议在URI中嵌入主版本号即可,例如:
http://api.example.com/v1/users整体原则是,尽量保持向下兼容,这样URI不会改变,老用户不需要迁移。
还有一个问题是,如果不带版本号访问应该套用哪个版本的接口?谷歌的做法是使用最老版本,这样就不会影响那些老用户了。
```* 有没有考虑API终止提供时的相关事项
```
停止API时应该让API返回410错误码,它代表接口不再对外公开。如果客户端是公司的产品,则可以强制客户端升级,避免停止API导致用户无法使用。
```* 有没有在文档里明确注明API的最低提供期限
```
错误的例子:该API 2018-06-01下线,请注意迁移。
正确的例子:
该API将继续维护12个月,请您尽快迁移。
错误的例子把期限说的太死了,而正确的例子则留了余地(比如再维护额外的12个月),使用者的感受会好很多。
```
* 有没有使用HTTPS来提供API
```
HTTP是明文传输,可以被任意劫持。HTTPS采用SSL通讯,保障数据安全。但是HTTPS要求客户端严格验证证书的真伪,否则中间人可以伪造证书实施攻击。
另外,作者强调HTTPS会导致请求变慢,但相比安全性仍然是值得做的。
```* 有没有认真执行JSON转义
```
一个这样的JSON,如果按照Content-Type: text/html被浏览器解析,其中的js就会被执行:
{"username": "<script>alert(1)"}好在大多数JSON库默认会在编码时会进行适当的转义,因此最终得不到执行:
{"username":"alert(1)<\/script>"}所以API应该返回完整转义过的JSON串,为了稳妥也应该设置Content-Type: application/json,避免浏览器将JSON当做html解析,导致攻击者得以实施XSS攻击。
```* 能不能识别X-Request-With首部,让浏览器无法通过SCRIPT元素读取JSON数据
```
假设https://api.example.com/v1/users/me是获取当前登录用户信息的接口。攻击者在自己的网站通过<script src="https://api.example.com/v1/users/me" language="vbscript">可以实施攻击。
因为接口返回的是JSON,而加载时指定了vb语言肯定是无法解析成功的,因此攻击者通过设置window.onerror = function(err) {}即可被浏览器回调,从而从错误信息中获取到用户信息。解决这个问题的方法是禁止通过script标签调用API,判定方法就是服务端判断请求中是否有Header X-Requested-With,因为Ajax请求默认会携带这个header而script不会。
```* 通过浏览器访问的API有没有使用XSRF token
```
XSRF称为跨站点请求伪造。攻击者在自己的网站做一个form表单,提交地址写为目标网站的表单提交地址。当受害者访问攻击者网站时,攻击者通过javascript自动提交form表单(form.submit),即可完成向目标网站的提交(想象这是一个转账表单)。
form表单提交不受同源策略(跨域)影响,因此可以达成上述攻击手段。
解决方法就是在表单里生成一次性的CSRF token放在隐藏字段中,并把token种植在用户cookie中,在用户提交表单到API时可以检查表单token和cookie中的token一致,则允许提交。
```* API在接收参数时有没有仔细检查非法的参数(负数等)
```
作者以减少用户积分的API为例,如果传入一个负数积分,会导致减法变成加法,导致用户积分越来越多。所以API需要严格校验参数是否合法。
```* 有没有做到即使请求重复发送,数据也不会多次更新
```
作者其实就是想表达幂等性,举了一个支付系统的例子,就不详细描述了。
```* 有没有在响应消息里添加各种增强安全性的首部
```
有很多header是作者建议总是加在API响应头里的,可以给浏览器很多建议,提升安全等级,就不一一描述了。比较重要的一点是set-cookie时的安全问题:
1)Secure属性:表示cookie只能在访问https链接时才能被发送给服务端,这样可以彻底避免cookie被攻击者在网络中嗅探到。
2)HttpOnly属性:cookie仅能供HTTP调用时使用,而不允许javascript直接获取cookie,这样可以避免网站出现XSS漏洞的时候,攻击者通过JS代码把用户的会话cookie盗走。
```* 有没有实施访问限速
```
限速是为了保护API服务,避免超过负载。限速一般是针对每个用户的,限速的单位是多少分钟内最多访问多少次。
从实际存储上可以采用Redis,key的数量大概是"API的数量 * 用户数量"。
API超出限速应该返回429 Too Many Requests的http code,最好还能给出Retry-After告知多久后可以继续使用。
```
* 对预想的用例来说限速的次数有没有设置得过少
```
一般来说,应该为开放的API开发一套dashboard管理后台,从而可以灵活的为不同的用户设置不同的限速值,以及查看实时速率以及剩余调用次数等信息。
```## 购买链接
[Web API设计与开发](https://amazon.cn/gp/product/B071GB3SLQ/ref=as_li_tl?ie=UTF8&camp=536&creative=3200&creativeASIN=B071GB3SLQ&linkCode=as2&tag=yuerblog-23&linkId=1e3108e1993e44c9a054120fe056b4b9)