检查清单
好的例子:http://api.example.com/search
坏的例子:http://api.example.com/service/api/search。
域名已经是api,在URI就没有必要重复一些毫无意义的单词。
不要轻易使用缩写:http://api.example.com/sv/u
使用更地道的英语表达,比如搜索接口一般用search而不是find,可以多参照一些国外大厂的API。
HTTP协议指定了:URL中除了schema和hostname不区分大小写,其他部分均大小写敏感。
因此,URI应该使用小写,禁止大小写混写。
比如获取某个商品信息的URI应该长这样:http://api.example.com/v1/items/12346
从URI直观的即可知道获取56780商品的URI是这样:http://api.example.com/v1/items/56780。
开发者对URI的行为理解是自然的,不费心的。
不要暴露服务端是哪种开发语言,下面是错误的例子:
http://api.example.com/cgi-bin/get_user.php?user=100
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设计风格。
作者强调REST风格。
HTTP方法表示"进行怎样的操作",URI表示"资源",HTTP和URI一起则表示"对什么资源做什么操作"。
GET:获取资源
获取ID=100的好友信息,
GET http://api.example.com/friends/100
POST:新增资源
添加一位好友,相当于新建一个好友关系:
POST http://api.example.com/friends
PUT:更新已有资源
更新ID=100的好友信息(例如:更新备注信息),
PUT http://api.example.com/friends/100
PATCH:更新部分资源
和PUT类似,只是强调更新资源的部分信息,不常用。
DELETE:删除资源
删除ID=100的好友信息:
DELETE http://api.example.com/friends/100
HEAD:获取资源的元信息
这个作者没有详细去说一个资源的元信息应该是什么样的。
感觉和自己设计的API不太一样吧?这就是REST风格,常见于国外各种大厂。
- URI里用到的单词所表达的意思是否和大部分API相同
有点重复,还是强调对于非英语母语的开发者,应该注意使用地道准确的单词。
比如,照片应该用photo而不是picture。
因为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
URL是会被urlencode编码的,所以不要在URI里使用空格(会被编码成+)、UTF-8字符、乱七八糟的符号等。
即不要影响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继续获取。(个人理解瀑布流页面比较适合)
很常见的认证规范,让用户通过大厂的账号系统登录,并授权给第三方获取用户信息的权限。
作者表达的意思是你如果对外提供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比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可以实现跨域HTTP调用,其原理是基于<script>加载一段服务端的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种方式,因为可以描述多个错误原因,并且很多大厂都是这么做的。
当服务端发生错误时,很多web框架会打印一个html错误信息页面。
对于API来说,当发生错误时也应该返回一个合法的JSON结构,因为客户端假设服务端返回JSON,返回HTML可能导致异常。
这一条规则有点重复,主要是指返回适当的http code。
当服务器需要停机维护时,按照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,这样避免一些严格的客户端出现解析失败。
浏览器有同源策略,禁止跨域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=3600
Cache-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。
作者介绍了语义化版本控制,通常版本号是a.b.c这样的,分别表示主版本号,次版本号,补丁版本号。
1,如果软件API没有变更,只是修复服务端BUG,那么就增加补丁版本号
2,对软件API实施了向下兼容的变更,增加次版本号
3,对软件API实施了不向下兼容的变更时,增加主版本号
- 有没有在URI里嵌入主版本编号,并且能够让人一目了然
对于Web API来说,作者建议在URI中嵌入主版本号即可,例如:
http://api.example.com/v1/users
整体原则是,尽量保持向下兼容,这样URI不会改变,老用户不需要迁移。
还有一个问题是,如果不带版本号访问应该套用哪个版本的接口?谷歌的做法是使用最老版本,这样就不会影响那些老用户了。
停止API时应该让API返回410错误码,它代表接口不再对外公开。
如果客户端是公司的产品,则可以强制客户端升级,避免停止API导致用户无法使用。
错误的例子:
该API 2018-06-01下线,请注意迁移。
正确的例子:
该API将继续维护12个月,请您尽快迁移。
错误的例子把期限说的太死了,而正确的例子则留了余地(比如再维护额外的12个月),使用者的感受会好很多。
HTTP是明文传输,可以被任意劫持。
HTTPS采用SSL通讯,保障数据安全。但是HTTPS要求客户端严格验证证书的真伪,否则中间人可以伪造证书实施攻击。
另外,作者强调HTTPS会导致请求变慢,但相比安全性仍然是值得做的。
一个这样的JSON,如果按照Content-Type: text/html被浏览器解析,其中的js就会被执行:
{"username": "<script>alert(1)</script>"}
好在大多数JSON库默认会在编码时会进行适当的转义,因此最终得不到执行:
{"username":"<script>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"></script>可以实施攻击。
因为接口返回的是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管理后台,从而可以灵活的为不同的用户设置不同的限速值,以及查看实时速率以及剩余调用次数等信息。