以前一篇文章写过REST服务介绍, 今天再次来自回顾一下. REST是一种架构风格. 首次出如今2000年Roy Fielding的博士论文中,Roy Fielding是 HTTP 规范的主要编写者之一。 论文中提到:“我这篇文章的写做目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,获得一个功能强、性能好、适宜通讯的架构。REST指的是一组架构约束条件和原则。” 若是一个架构符合REST的约束条件和原则,咱们就称它为RESTful架构。php
RESTFul架构应该遵循统一接口原则,统一接口包含了一组受限的预约义的操做,不论什么样的资源,都是经过使用相同的接口进行资源的访问。接口应该使用标准的HTTP方法如GET,PUT和POST,并遵循这些方法的语义。
若是按照HTTP方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如GET和HEAD请求都是安全的, 不管请求多少次,都不会改变服务器状态。而GET、HEAD、PUT和DELETE请求都是幂等的,不管对资源操做多少次, 结果老是同样的,后面的请求并不会产生比第一次更多的影响。html
下面列出了GET,DELETE,PUT和POST的典型用法:前端
变动时获取表示(缓存)java
200(OK) - 表示已在响应中发出python
若是没有被修改,则不过更新资源(乐观锁)git
200(OK)- 若是现有资源已被更改github
若是未被修改,则更新资源(乐观锁)web
200 (OK)- 若是已存在资源被更改数据库
删除资源express
200 (OK)- 资源已被删除
有了上面的铺垫,再讨论REST里边的状态转移就会很容易理解了。 不过,咱们先来讨论一下REST原则中的无状态通讯原则。初看一下,好像自相矛盾了,既然无状态,何来状态转移一说?
其实,这里说的无状态通讯原则,并非说客户端应用不能有状态,而是指服务端不该该保存客户端状态。
实际上,状态应该区分应用状态和资源状态,客户端负责维护应用状态,而服务端维护资源状态。 客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。 服务端不须要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态。 这种无状态通讯原则,使得服务端和中介可以理解独立的请求和响应。 在屡次请求中,同一客户端也再也不须要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。
但有时候咱们会作出违反无状态通讯原则的设计,例如利用Cookie跟踪某个服务端会话状态,常见的像J2EE里边的JSESSIONID。 这意味着,浏览器随各次请求发出去的Cookie是被用于构建会话状态的。 固然,若是Cookie保存的是一些服务器不依赖于会话状态便可验证的信息(好比认证令牌),这样的Cookie也是符合REST原则的。
状态转移到这里已经很好理解了, “会话”状态不是做为资源状态保存在服务端的,而是被客户端做为应用状态进行跟踪的。客户端应用状态在服务端提供的超媒体的指引下发生变迁。服务端经过超媒体告诉客户端当前状态有哪些后续状态能够进入。 这些相似“下一页”之类的连接起的就是这种推动状态的做用–指引你如何从当前状态进入下一个可能的状态。
REST API通常用来将某种资源和容许的对资源的操做暴露给外界,使调用者可以以正确的方式操做资源。这里,在输入输出的处理上,要符合HTTP/1.1(不久的未来,要符合HTTP/2.0)的RFC,保证接口的一致性。这里主要讲输入的method/headers和输出的status code。
HTTP协议提供了不少methods来操做数据:
GET: 获取某个资源,GET操做应该是幂等(idempotence)的,且无反作用。
POST: 建立一个新的资源。
PUT: 替换某个已有的资源。PUT操做虽然有反作用,但其应该是幂等的。
PATCH(RFC5789): 修改某个已有的资源。http://tools.ietf.org/html/rfc5789
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 API时,
简单说来就是一个操做符合幂等性,那么相同的数据和参数下,执行一次或屡次产生的效果(反作用)是同样的。
如今大多的REST framwork对HTTP methods都有正确的支持,有些旧的framework可能未必对PATCH有支持,须要注意。若是本身手写REST API,必定要注意区分POST/PUT/PATCH/DELETE的应用场景。
The difference between the PUT and PATCH requests is reflected in the way the server processes the enclosed entity to modify the resource identified by the Request-URI. In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version.
The PUT method is already defined to overwrite a resource with a complete new body, and cannot be reused to do partial changes. Otherwise, proxies and caches, and even clients and servers, may get confused as to the result of the operation. POST is already used but without broad interoperability (for one, there is no standard way to discover patch format support)
A PATCH request can be issued in such a way as to be idempotent, which also helps prevent bad outcomes from collisions between two PATCH requests on the same resource in a similar time frame. Collisions from multiple PATCH requests may be more dangerous than PUT collisions because some patch formats need to operate from a known base-point or else they will corrupt the resource. Clients using this kind of patch application SHOULD use a conditional request such that the request will fail if the resource has been updated since the client last accessed the resource. For example, the client can use a strong ETag [RFC2616] in an If-Match header on the PATCH request.
不少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。这个头很是重要,下文详解。
不少REST API犯下的另外一个错误是:返回数据时不遵循RFC定义的status code,而是一概200 ok + error message。这么作在client + API都是同一公司所为还凑合可用,但一旦把API暴露给第三方,不但贻笑大方,还会留下诸多互操做上的隐患。
以上仅仅是最基本的一些考虑,要作到彻底符合RFC,除了参考RFC自己之外,erlang社区的webmachine或者clojure下的liberator都是不错的实现,是目前为数很少的REST API done right的library/framework。
请查看大图的flow的SVG
前面说过,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,HTTP Digest,OAuth,HMAC Auth, JWT等,其核心思想都是验证某个请求是由一个合法的请求者发起。Basic Auth(http://www.ietf.org/rfc/rfc2617.txt)会把用户的密码暴露在网络之中,并不是最安全的解决方案,OAuth的核心部分与HMAC Auth差很少,只不过多了不少与token分发相关的内容。
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+ Figure 1: Abstract Protocol Flow The abstract OAuth 2.0 flow illustrated in Figure 1 describes the interaction between the four roles and includes the following steps: (A) The client requests authorization from the resource owner. The authorization request can be made directly to the resource owner (as shown), or preferably indirectly via the authorization server as an intermediary. (B) The client receives an authorization grant, which is a credential representing the resource owner's authorization, expressed using one of four grant types defined in this specification or using an extension grant type. The authorization grant type depends on the method used by the client to request authorization and the types supported by the authorization server. (C) The client requests an access token by authenticating with the authorization server and presenting the authorization grant. (D) The authorization server authenticates the client and validates the authorization grant, and if valid, issues an access token.
示例参考:淘宝的OAUTH
回到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=
以下是Amazon HMAC
在作HMAC的时候,request headers中的request method,request URI,Date/X-Auth-Timestamp等header会被计算在HMAC中。将时间戳计算在HMAC中的好处是能够防止replay攻击。客户端和服务器之间的UTC时间正常来讲误差很小,那么,一个请求携带的时间戳,和该请求到达服务器时服务器的时间戳,中间差异太大,超过某个阈值(好比说120s),那么能够认为是replay,服务器主动丢弃该请求。
使用HMAC能够很大程度上防止DOS攻击 —— 无效的请求在验证HMAC阶段就被丢弃,最大程度保护服务器的计算资源。
http://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
HMAC Auth尽管在保证请求的一致性上很是安全,能够用于鉴别请求是否由合法的请求者发起,但请求的数据和服务器返回的响应都是明文传输,对某些要求比较高的API来讲,安全级别还不够。这时候,须要部署HTTPS。在其之上再加一层屏障。
目前应该是使用TLS1.2 https://tools.ietf.org/html/rfc5246
为你的API启用其它任何你的组织已部署的web应用一样的安全机制。好比说,若是你在web前端过滤XSS,你必须对你的API也这样作,最好是使用一样的工具。
不要使用你本身的安全。使用那些被互审过测试过的框架或现有的包...
除非你的API是一个免费的,只读的公开API,不然不要使用单一的基于密钥的验证。这不够,须要加上密码要求。
不要放过未加密的静态密钥。若是你使用基本的HTTP而且在线路上发送的,请加密。
理想的状况下,使用基于哈布的消息验证码(HMAC),由于它最安全。
还能够参考REST_Security_Cheat_Sheet, Security_testing_for_REST_applications
POST和PUT在建立资源的区别在于,所建立的资源的名称(URI)是否由客户端决定。 例如为个人博文增长一个java的分类,生成的路径就是分类名/categories/java,那么就能够采用PUT方法。 不过不少人直接把POST、GET、PUT、DELETE直接对应上CRUD,例如在一个典型的rails实现的RESTFul应用中就是这么作的。 我认为,这是由于rails默认使用服务端生成的ID做为URI的缘故,而很多人就是经过rails实践REST的,因此很容易形成这种误解。
的确有这种状况,特别是一些比较古老的基于浏览器的客户端,只能支持GET和POST两种方法。 在实践上,客户端和服务端均可能须要作一些妥协。例如rails框架就支持经过隐藏参数_method=DELETE来传递真实的请求方法, 而像Backbone这样的客户端MVC框架则容许传递_method传输和设置X-HTTP-Method-Override头来规避这个问题。
统一接口并不阻止你扩展方法,只要方法对资源的操做有着具体的、可识别的语义便可,并可以保持整个接口的统一性。 像WebDAV就对HTTP方法进行了扩展,增长了LOCK、UPLOCK等方法。而github的API则支持使用PATCH方法来进行issue的更新,例如:
PATCH /repos/:owner/:repo/issues/:number
不过,须要注意的是,像PATCH这种不是HTTP标准方法的,服务端须要考虑客户端是否可以支持的问题。
统一资源接口要求使用标准的HTTP方法对资源进行操做,因此URI只应该来表示资源的名称,而不该该包括资源的操做。 通俗来讲,URI不该该使用动做来描述。例如,下面是一些不符合统一接口要求的URI:
DELETE /deleteUser/1
安全性不表明请求不产生反作用,例如像不少API开发平台,都对请求流量作限制。像github,就会限制没有认证的请求每小时只能请求60次。 但客户端不是为了追求反作用而发出这些GET或HEAD请求的,产生反作用是服务端“自做主张”的。 另外,服务端在设计时,也不该该让反作用太大,由于客户端认为这些请求是不会产生反作用的。
即便你按各个动词的本来意图来使用它们,你仍能够轻易禁止缓存机制。 最简单的作法就是在你的HTTP响应里增长这样一个报头: Cache-control: no-cache。 可是,同时你也对失去了高效的缓存与再验证的支持(使用Etag等机制)。 对于客户端来讲,在为一个REST式服务实现程序客户端时,也应该充分利用现有的缓存机制,以避免每次都从新获取表示。
如上图所示,HTTP的响应代码可用于应付不一样场合,正确使用这些状态代码意味着客户端与服务器能够在一个具有较丰富语义的层次上进行沟通。 例如,201(“Created”)响应代码代表已经建立了一个新的资源,其URI在Location响应报头里。 假如你不利用HTTP状态代码丰富的应用语义,那么你将错失提升重用性、加强互操做性和提高松耦合性的机会。 若是这些所谓的RESTFul应用必须经过响应实体才能给出错误信息,那么SOAP就是这样的了,它就可以知足了。
作到了接口一致性(符合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。
今天先到这儿,但愿对您有参考做用, 您可能感兴趣的文章:
构建高效的研发与自动化运维
某大型电商云平台实践
互联网数据库架构设计思路
IT基础架构规划方案一(网络系统规划)
餐饮行业解决方案之客户分析流程
餐饮行业解决方案之采购战略制定与实施流程
餐饮行业解决方案之业务设计流程
供应链需求调研CheckList
企业应用之性能实时度量系统演变
若有想了解更多软件设计与架构,系统 IT,企业信息化 资讯,请关注个人微信订阅号:
做者:Petter Liu
出处:http://www.cnblogs.com/wintersun/
本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。
该文章也同时发布在个人独立博客中-Petter Liu Blog。