撰写合格的REST API

两周前由于公司一次裁员,好几我的的活都被按在了我头上,这其中的一大部分是一系列REST API,撰写者号称基本完成,我测试了一下,发现尽管从功能的角度来讲,这些API实现了spec的显式要求,可是从实际使用的角度,欠缺的东西太多(各类各样的隐式需求)。REST API是一个系统的backend和frontend(或者3rd party)打交道的通道,承前启后,有不少不少隐式需求,好比调用接口与RFC保持一致,API的内在和外在的安全性等等,并不是提供几个endpoint,返回相应的json数据那么简单。仔细研究了原做者的代码,发现缺失的东西实在太多,每一个API基本都在各自为战,与其修补,不如重写(并不是是程序员相轻的缘故),因而我花了一整周,重写了全部的API。稍稍总结了些经验,在这篇文章里讲讲如何撰写「合格的」REST API。python

  RFC一致性git

  REST API通常用来将某种资源和容许的对资源的操做暴露给外界,使调用者可以以正确的方式操做资源。这里,在输入输出的处理上,要符合HTTP/1.1(不久的未来,要符合HTTP/2.0)的RFC,保证接口的一致性。这里主要讲输入的method/headers和输出的status code。程序员

  Methodsgithub

  HTTP协议提供了不少methods来操做数据:web

GET: 获取某个资源,GET操做应该是幂等(idempotence)的,且无反作用。数据库

POST: 建立一个新的资源。django

PUT: 替换某个已有的资源。PUT操做虽然有反作用,但其应该是幂等的。json

PATCH(RFC5789): 修改某个已有的资源。flask

DELETE:删除某个资源。DELETE操做有反作用,但也是幂等的。缓存

  幂等在HTTP/1.1中定义以下:

Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

 简单说来就是一个操做符合幂等性,那么相同的数据和参数下,执行一次或屡次产生的效果(反作用)是同样的。

  如今大多的REST framwork对HTTP methods都有正确的支持,有些旧的framework可能未必对PATCH有支持,须要注意。若是本身手写REST API,必定要注意区分POST/PUT/PATCH/DELETE的应用场景。

  Headers

  不少REST API犯的比较大的一个问题是:不怎么理会request headers。对于REST API,有一些HTTP headers很重要:

Accept:服务器须要返回什么样的content。若是客户端要求返回"application/xml",而服务器端只能返回"application/json",那么最好返回status code 406 not acceptable(RFC2616),固然,返回application/json也并不违背RFC的定义。一个合格的REST API须要根据Accept头来灵活返回合适的数据。

If-Modified-Since/If-None-Match:若是客户端提供某个条件,那么当这条件知足时,才返回数据,不然返回304 not modified。好比客户端已经缓存了某个数据,它只是想看看有没有新的数据时,会用这两个header之一,服务器若是不理不睬,依旧作足全套功课,返回200 ok,那就既不专业,也不高效了。

If-Match:在对某个资源作PUT/PATCH/DELETE操做时,服务器应该要求客户端提供If-Match头,只有客户端提供的Etag与服务器对应资源的Etag一致,才进行操做,不然返回412 precondition failed。这个头很是重要,下文详解。

  Status Code

  不少REST API犯下的另外一个错误是:返回数据时不遵循RFC定义的status code,而是一概200 ok + error message。这么作在client + API都是同一公司所为还凑合可用,但一旦把API暴露给第三方,不但贻笑大方,还会留下诸多互操做上的隐患。

  以上仅仅是最基本的一些考虑,要作到彻底符合RFC,除了参考RFC自己之外,erlang社区的webmachine或者clojure下的liberator都是不错的实现,是目前为数很少的REST API done right的library/framework。

输入图片说明 (liberator的decision tree,沿袭了webmachine的思想,请自行google其文档查看大图)

  安全性

  前面说过,REST API承前启后,是系统暴露给外界的接口,因此,其安全性很是重要。安全并单单不意味着加密解密,而是一致性(integrity),机密性(confidentiality)和可用性(availibility)。

  请求数据验证

  咱们从数据流入REST API的第一步 —— 请求数据的验证 —— 来保证安全性。你能够把请求数据验证当作一个巨大的漏斗,把没必要要的访问通通过滤在第一线:

Request headers是否合法:若是出现了某些不应有的头,或者某些必须包含的头没有出现或者内容不合法,根据其错误类型一概返回4xx。好比说你的API须要某个特殊的私有头(e.g. X-Request-ID),那么凡是没有这个头的请求一概拒绝。这能够防止各种漫无目的的webot或crawler的请求,节省服务器的开销。

Request URI和Request body是否合法:若是请求带有了不应有的数据,或者某些必须包含的数据没有出现或内容不合法,一概返回4xx。好比说,API只容许querystring中含有query,那么"?sort=desc"这样的请求须要直接被拒绝。有很多攻击会在querystring和request body里作文章,最好的对应策略是,过滤全部含有不应出现的数据的请求。

  数据完整性验证

  REST API每每须要对backend的数据进行修改。修改是个很可怕的操做,咱们既要保证正常的服务请求可以正确处理,还须要防止各类潜在的攻击,如replay。数据完整性验证的底线是:保证要修改的数据和服务器里的数据是一致的 —— 这是经过Etag来完成。

  Etag能够认为是某个资源的一个惟一的版本号。当客户端请求某个资源时,该资源的Etag一同被返回,而当客户端须要修改该资源时,须要经过"If-Match"头来提供这个Etag。服务器检查客户端提供的Etag是否和服务器同一资源的Etag相同,若是相同,才进行修改,不然返回412 precondition failed。

  使用Etag能够防止错误更新。好比A拿到了Resource X的Etag X1,B也拿到了Resource X的Etag X1。B对X作了修改,修改后系统生成的新的Etag是X2。这时A也想更新X,因为A持有旧的Etag,服务器拒绝更新,直至A从新获取了X后才能正常更新。

  Etag相似一把锁,是数据完整性的最重要的一道保障。Etag能把绝大多数integrity的问题扼杀在摇篮中,固然,race condition仍是存在的:若是B的修改还未进入数据库,而A的修改请求正好经过了Etag的验证时,依然存在一致性问题。这就须要在数据库写入时作一致性写入的前置检查。

  访问控制

  REST API须要清晰定义哪些操做可以公开访问,哪些操做须要受权访问。通常而言,若是对REST API的安全性要求比较高,那么,全部的API的全部操做均需获得受权。

  在HTTP协议之上处理受权有不少方法,如HTTP BASIC Auth,OAuth,HMAC Auth等,其核心思想都是验证某个请求是由一个合法的请求者发起。Basic Auth会把用户的密码暴露在网络之中,并不是最安全的解决方案,OAuth的核心部分与HMAC Auth差很少,只不过多了不少与token分发相关的内容。这里咱们主要讲讲HMAC Auth的思想。

  回到Security的三个属性:一致性,机密性,和可用性。HMAC Auth保证一致性:请求的数据在传输过程当中未被修改,所以能够安全地用于验证请求的合法性。

  HMAC主要在请求头中使用两个字段:Authorization和Date(或X-Auth-Timestamp)。Authorization字段的内容由":"分隔成两部分,":"前是access-key,":"后是HTTP请求的HMAC值。在API受权的时候通常会为调用者生成access-key和access-secret,前者能够暴露在网络中,后者必须安全保存。当客户端调用API时,用本身的access-secret按照要求对request的headers/body计算HMAC,而后把本身的access-key和HMAC填入Authorization头中。服务器拿到这个头,从数据库(或者缓存)中取出access-key对应的secret,按照相同的方式计算HMAC,若是其与Authorization header中的一致,则请求是合法的,且未被修改过的;不然不合法。

GET /photos/puppy.jpg HTTP/1.1 Host: johnsmith.s3.amazonaws.com Date: Mon, 26 Mar 2007 19:37:58 +0000

Authorization: AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg= 输入图片说明

 在作HMAC的时候,request headers中的request method,request URI,Date/X-Auth-Timestamp等header会被计算在HMAC中。将时间戳计算在HMAC中的好处是能够防止replay攻击。客户端和服务器之间的UTC时间正常来讲误差很小,那么,一个请求携带的时间戳,和该请求到达服务器时服务器的时间戳,中间差异太大,超过某个阈值(好比说120s),那么能够认为是replay,服务器主动丢弃该请求。

  使用HMAC能够很大程度上防止DOS攻击 —— 无效的请求在验证HMAC阶段就被丢弃,最大程度保护服务器的计算资源。

  HTTPS

  HMAC Auth尽管在保证请求的一致性上很是安全,能够用于鉴别请求是否由合法的请求者发起,但请求的数据和服务器返回的响应都是明文传输,对某些要求比较高的API来讲,安全级别还不够。这时候,须要部署HTTPS。在其之上再加一层屏障。

  其余

  作到了接口一致性(符合RFC)和安全性,REST API能够算得上是合格了。固然,一个实现良好的REST API还应该有以下功能:

rate limiting:访问限制。

metrics:服务器应该收集每一个请求的访问时间,到达时间,处理时间,latency,便于了解API的性能和客户端的访问分布,以便更好地优化性能和应对突发请求。

docs:丰富的接口文档 —— API的调用者须要详尽的文档来正确调用API,能够用swagger来实现。

hooks/event propogation:其余系统可以比较方便地与该API集成。好比说添加了某资源后,经过kafka或者rabbitMQ向外界暴露某个消息,相应的subscribers能够进行必要的处理。不过要注意的是,hooks/event propogation可能会破坏REST API的幂等性,须要当心使用。

  各个社区里面比较成熟的REST API framework/library: Python: django-rest-framework(django),eve(flask)。各有千秋。惋惜python没有好的相似webmachine的实现。

Erlang/Elixir: webmachine/ewebmachine。 Ruby: webmachine-ruby。 Clojure:liberator。

  其它语言接触很少,就不介绍了。能够经过访问该语言在github上相应的awesome repo(google awesome XXX,如awesome python),查看REST API相关的部分。

相关文章
相关标签/搜索