设计一款“实践派”的REST API

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Roy Thomas Fielding博士在其著名的论文Architectural Styles andthe Design of Network-based Software Architectures中,详细描述了几种常见的软件架构风格,其中第5章Representational State Transfer就是大名鼎鼎的REST风格。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"随着Web 2.0和微服务的兴起,以及SOAP Web Services的没落,REST API开始大行其道,在JSON紧凑、易读、高效率的加持下,使得该API风格几乎成为现代Remoting通信技术的事实标准。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一万个人眼中就有一万个哈姆雷特。对REST API风格的的理解和应用,历来存在许多分歧和五花八门的使用方式,出现了譬如“学院派“设计,一切都要遵循理论并与之严格对齐,当然也涌现了不少反范式的民间设计。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文结合实际的生产环境经验,试图说明如何设计一款“实践派“的REST  API。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"以资源为中心","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和面向RPC的 SOAP Web Services不同,REST  API的核心是资源(Resource),掌握了资源就相当于牵住了牛鼻子。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"资源是一个广义的概念,可以是业务实体,也可以是一个事件或动作,资源一般用URI来描述。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"资源的分类及描述,一般和业务领域建模很相似。以电商行业为例,资源可以是用户(User)、商品(Product)、订单(Order)、结算(Checkout)、运输(Shipment)等;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API的核心理念就是围绕资源而进行的一系列操作及状态变化。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"公共参数","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一款好的REST  API应该将公共参数和业务参数分离设计,并支持独立变化。公共参数建议以header或query_string的方式来进行传递。常见的公共参数有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客户端相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_key,后端下发给客户端的唯一标识;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_secret,与app_key强相关,一般不在网络中传输;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       platform,Android|iOS|Web等API使用方的平台识别码;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       channel,安装包渠道编号,一般用于APP客户端;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"设备相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_id,客户端的设备号;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_os,客户端的设备操作系统;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_version,客户端的设备版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       androidid/imei/idfa,Android|iOS等客户端的标识;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_mac,网卡物理地址;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用户相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       uid,公司统一用户ID;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"版本相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_version,APP正式版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       gray_version,APP灰度版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       internal_version,APP内部版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"地理相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       ip,外网IP地址,一般客户端拿不到,需服务端从负载均衡获取;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       longitude,GPS经度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       latitude,GPS纬度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"国际化相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       language,用户的语言;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       country,用户的国家;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       currency,用户的货币;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网络相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       net_status,网络状态,如3G/4G/5G/Wifi等;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安全相关:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       signature,API摘要,防止伪造请求,签名最好加入app_secret增加破解难度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       access_token,用户token,防止伪造身份,一般由公司SSO下发且带TTL;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       timestamp,时间戳,防止重放;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"命名风格","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从最佳实践角度来说,URL命名风格建议遵循以下一些原则:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       以名词命名。如/api/products,/api/orders,/api/users。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       以复数命名。这只是一种约定俗成的风格,便於潜在的扩展需要。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       需要有统一的API根。如/api/business1,/api/business2。这样的好处是能做好望文生义,见到API即可了解该接口来自哪个服务,也便于监控和日志统计。在微服务的架构设计准则下,API根尤为重要。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       尽可能采用蛇形(snake_case)而不是驼峰(camelCase)来命名URL;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       采用HTTP Method来操作资源。通常采用GET来读取资源,POST创建资源,PUT创建或全量更新资源,PATCH局部更新资源,DELETE删除资源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意的是,在实际使用场景中,我们并不是真的要坚持这些“理论派“,完全可以不必拘泥于这些约束,可以灵活变通,比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       有些场景没法通过名词来描绘 ,如用户登录。这时可以使用/api/users/login来命名;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       PUT和PATCH有相似之处,是否可以用PUT来代替PATCH?也未尝不可,更有甚者,可以采用POST来代替;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"安全性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不安全的REST  API会导致信息泄露,给不法分子可乘之机,严重的还会损害公司信誉和形象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个安全的REST API需要具备哪些因素呢?这里列出几种比较实用的安全设计准则:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"采用HTTPS","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从现在开始,全体API就应该采用HTTPS传输。完全不用担心由此带来的资源消耗和耗时增加。API网关做SSL卸载所带来的额外CPU开销,完全可以通过增加更多的服务器,通过水平扩展来分摊压力。而耗时的增加相对于业务层延迟来说,几乎可以忽略不计,这种情况更多应该在应用层做耗时调优,而不用理会接入层的开销。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"摘要(Digest)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API摘要本质上是一种不可逆的指纹,目的是实现API请求不可篡改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API摘要有时候也被通俗的称为接口签名,注意这里和密码学上的数字签名严格上说意义不太一样,数字签名是指利用私钥加密签发,接收方用公钥解密。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里介绍一种实用的摘要算法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API提供方(Provider)提供app_key和app_secret给API使用方(Consumer);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方将请求参数和app_key、app_secret、时间戳(timestamp)等按一定的算法进行混合、排序,生成一个字符串;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方对上一步生成的字符串,按常见的摘要算法(如MD5、SHA-256等)进行哈希计算,得到指纹;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方将上一步得到的指纹,以signature=的形式,追加到请求参数中;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API提供方接收到请求后,采用同样的方式,进行哈希计算,再和signature进行比较,从而达到验证签名的目的;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意的是,app_secret在API使用方仅参与摘要计算,起到一个随机盐的作用,但不随请求传输。这样的好处是防止被截获。另外API提供方已经存在app_secret,自然不需要请求方再次传递。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好了,上面的摘要算法真的固若金汤、牢不可破吗?答案是否定的,没有绝对的安全。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最常见的威胁就是重放攻击。尽管我们已经设置了timestamp时间戳,服务端也完全可以利用timestamp和当前时间的间隔来限制重复请求,但也会存在两个问题:客户端和服务端可能会存在时钟不一致,另外在有效的时间窗口内依然可以重放。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解决方案有多种,从本质上说应该业务层面做好幂等,限制重复请求带来的副作用。另外也可以考虑服务端给调用方分配一个计数,并在内部记录和校验,该计数只可以递增,每次攻击者试图重放API就必须递增该计数,然而摘要算法是不公开的,这就使得重放无法实现。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外的问题就是如果API使用方的代码被反编译,摘要算法和app_secret被同时破解,黑客就可以随意伪造API请求了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解决方案是采用动态随机盐,由API提供方为每个设备或用户生成动态随机盐并周期性刷新,记录到数据库中进行校验。即使代码被破解,攻击者也无计可施。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"认证(Authentication)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST  API的认证是为了验明用户身份,实现用户身份的不可抵赖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一种比较实用的做法是采用令牌(access_token)机制。即用户登录后,由API提供方(通常是用户或Passport中心)颁发一个令牌给使用方,该token安全级别较高,全局唯一,不可逆,可周期性更新或支持续借(renew)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当API调用方将access_token加入到API请求后,API提供方会将读取该参数,并查询后台进行用户身份确认,从而实现认证功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为什么采用token机制呢?原因很简单,用户身份一般由用户名和密码构成,API客户端不可能将这些信息频繁传到服务端,这会增加信息泄密的风险。而token是不可理解的密文,且可以更新,故安全级别能得到较大提升。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鉴权(Authorization)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"仅身份认证是不够的,REST  API的鉴权是为了解决用户权限问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如没有鉴权,则用户可以操作任何资源,这显然是不安全的。常见做法是检查用户的权限与角色,确认对资源的操作权限。如普通用户只能删除、修改自己发布的内容,管理员则可以操作任何用户的数据。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"国际化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一些国际化的业务中,如跨境电商,往往需要REST API支持国际化。常见的实用做法是API调用方采集用户的国际化属性,加入API请求参数。API提供方读取这些属性,存储到后台,或是直接在业务层面使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常见的国际化属性有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       语言。通常会采用language = EN | ZH | FR 等形式进行参数传递;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       国家。通常采用country = CN | US | BR 等形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       货币。通常采用currency = USD | CNY | EUR 等形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       时区。通常采用time_zone = GMT+8:00形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"兼容性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兼容性是REST  API开发者的必备素质,很多线上回归错误(往往还很隐晦)都是由于API没有考虑向下兼容造成的。因此需要考虑一些设计准则:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       只加字段,不删除字段,不修改字段名称;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       任何时候开发新功能,需要考虑到版本控制,即新功能只限于新版本,除非明确老版本也能使用而不受影响;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"幂等性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幂等性并不是绝对的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们考虑下HTTP 常见Method的幂等性,如GET /PUT/PATHCH/DELETE,这些操作天然具备幂等性语义,即多次操作,不会产生副作用。而POST操作,默认在多次操作下会多次创建资源,因此不是幂等的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,业务实现是靠技术人员实现的,因此完全可以人为控制幂等性。所以,这里并不建议过于教条,而应该根据实际情况来决定幂等性语义。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"过滤","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个设计良好的REST  API应该具备过滤能力。一种实用的做法就是通过传业务参数来控制过滤逻辑,如id=123456,user_name=xxx等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一种层面的过滤是指返回的字段,应该可以由API调用方来控制,如fields=user_name, user_age, user_city等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样做的好处是显而易见的,一方面可以减少服务端的资源消耗,特别是存储的查询压力,另一方面也可以减少网络带宽的占用和API使用方的反序列化成本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"排序","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"业务场景往往会有排序的诉求,如商品展示可以按发布时间、热度、销售量进行排序。REST  API应该能支持灵活易用的排序方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一种常见的最佳实践就是在API请求中增加sort字段,并且支持多维度排序。如按时间正向排序,且按积分反向排序,就可以表示成:sort=+time,-score。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"分页","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"几乎所有的产品形态都需要分页,分页功能看似简单,实际上做起来很复杂。比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       如何保证每一页的数据不存在重复或丢失?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       大数据量的集合(千万级别),如何实现深度分页,且效率不受影响?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       如何实现跳页,即随机读取某一页数据?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这些问题不在本文讨论范围内,感兴趣的读者可以自行搜寻答案。这里介绍两种常见的分页请求参数方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       采用页码方式,如offset/limit或page_no/page_size。这种方式很常见,通常对于大的数据量来说,随着页码的增加分页效率会递减。当然也可以采用一些优化的技巧,如MySQL采用覆盖索引的子查询;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       采用游标方式,如cursor。这种方式相对简单,适合单维度、固定排序的数据分页。好处是时间复杂度可以是常量级别,弊端是不可以随机读取,只能从头顺序访问,当然也可以通过其他方式拿到cursor直接去访问下一页;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"状态码","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST  API基于HTTP,自然需要定义响应的状态码,常见的预定义状态码大致可参考:","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

1XX

\n
\n

100-101

\n
\n

信息提示

\n
\n

2XX

\n
\n

200-206

\n
\n

成功

\n
\n

3XX

\n
\n

300-305

\n
\n

重定向

\n
\n

4XX

\n
\n

400-415

\n
\n

客户端错误

\n
\n

5XX

\n
\n

500-505

\n
\n

服务器错误

\n
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在实际REST API设计中,可以灵活运用这些状态码,当然也大可不必拘泥于这些预设的状态码并咬文嚼字。常见的以200和4XX、5XX使用较多。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,还可以自定义一些非保留状态码如6XX、7XX,用于一些特殊使用场景。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"业务码","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"业务码是非常重要的信息表达方式,API使用方肯定不希望在出错时,只是看到一个笼统的提示:“出错了”,而是可以读取到具体的错误码和对应的提示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常,REST API返回报文会使用一个固定的格式,常见的有:","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"\n\n\n\n
\n

“code”: “S0000000”,

\n

“message”: “api invoke success”,

\n

“data”:  {xxxxxx}

\n
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中code和messaage就是业务码和提示信息发挥作用的地方。我们可以定义一个业务码字典和对应的提示信息。如E000001代表缺少参数xxx,E000002代表订单已被删除等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"谨记几条原则:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       永远不要把程序内部异常或错误抛给API调用方,而是采用业务码优雅的提示;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       当输出错误码时,HTTP 状态码也应该同步调整,比如输出5XX,这样可以让监控系统快速发现问题;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章