Categories
程式開發

徹底明白如何設計一個良好的API



{“ type”:“ doc”,“ content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:0,”align”:null,”origin”:null}},{“type”:”heading”,”attrs”:{“align”:null,”level”:3},”content”:[{“type”:”text”,”text”:”背景”}]},{“ 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 的统一认知?又该如何设计一个良好的 API 接口?随着业务的演进,如何设计一个有兼容性的API?面对多种客户端,如何设计一个处处适用的 API 呢?”}]},{“ 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”:”RESTful 是目前最流行的 API 设计规范,通过一定的规范可以解决上面这些问题,对于 RESTful 架构的理解可以参考阮一峰老师的这篇文章(http://www.ruanyifeng.com/blog/2011/09/restful.html),简单一句话就是对服务器端资源进行操作,实现”表现层状态转化”。URI(统一资源定位符)代表一种资源,它可以是一段文本、一张图片、一首歌曲、一种服务;每种资源对应一个特定的 URI,存在着这种对应关系。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:3},“ content”:[{“type”:”text”,”text”:”什么样的 API 是好的设计呢?”}]},{“ 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”:”我们要遵循以下几个原则:”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”标准化”}]},{“ 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”:”RESTful API 通过 GET POST PUT DELETE 等方式对服务器的资源进行操作,因此在定义 API 的时候需要明确定义出:请求方式、版本、资源名称和资源ID,格式如: GET http://{host}/{version}/{resources}/{resource_id},如查看用户编码为1 的用户信息 GET /v1/users/1。”}]},{“ 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”:”标准化主要从 URL 设计、状态码的使用、服务器响应码使用,具体可参考阮一峰老师的RESTful API 最佳实践(https://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html),千万不要自定义状态码。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”兼容性”}]},{“ 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 是非常重要的,如果接口不能够向下兼容,业务就会受到很大影响,产品涵盖 Android iOS PC 等客户端,用户需要产品升级到最新的版本,才能更好地使用,这种情况下引入版本的概念,实现 API 的兼容性。可以参考一些开放 API 的设计,通过版本号或一些开关参数来支持一些新功能。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”抽象性”}]},{“ 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”:”接口抽象都是基于业务需求,定义清晰的业务问题域模型,并建立起和某个问题的对应,实现抽象,可以很好地屏蔽具体的业务实现细节,提供更好的可扩展性。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”简单性”}]},{“ 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 接口,以及这些 API 接口的调用细节,如外观接口对各个微服务统一管理(进行业务封装与整合),对调用方提供一个简单的 API ,客户端只需要调用一个接口,而不用关心里面的细节。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”高性能”}]},{“ 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”:”外观接口虽然保证了简单性,但是增加了服务端的业务复杂度,多服务之间的聚合,导致接口性能不好,还要考虑入参字段的各种组合,会不会导致数据库的性能问题,有时候我们暴露了过多字段给外部组合使用,有可能导致数据库没有相应的索引,而发生全表扫描,因此,我们只提供存在索引的字段组合,给外部调用,要求索引字段为必填字段,这样保障能正常使用索引来确保性能”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”幂等性”}]},{“ 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”:”幂等性(Idempotent)在这里表示发送一次和多次请求引起的边界效应是一致的。如在网速不够快的情况下,客户端发送一个请求后没有立即得到响应,由于不能确定是否请求是否被成功提交,所以有可能会再次发送另一个相同的请求,幂等性决定了第二个请求是否有效。”}]},{“ 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”:”HTTP 7种方法中的(GET、HEAD 和 OPTIONS)均是幂等方法。由于 DELETE 和 PATCH 请求操作的是现有的某个资源,所以它们是幂等方法。对于 PUT 请求,只有在对应资源不存在的情况下服务器才会进行添加操作,否则只作修改操作,对于是否幂等操作,还要看这个修改操作是相对的还是绝对的,比如更新年龄,是在原值的基础上加1操作,还是更新成一个新的年龄了。至于最后一种 POST,由于它总是进行添加操作,如果服务器接收到两次相同的 POST 操作,将导致两个相同的资源被创建,所以这是一个非幂等的方法。”}]},{“ 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”:”怎么解决幂等性问题呢?(简单场景:表单重复提交)利用 Redisson 实现分布式锁,并防止重复提交。这个话题可以单独写一篇文章总结一下。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:4},“ content”:[{“type”:”text”,”text”:”安全性”}]},{“ 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”:”对于提供公网域名进行访问,而且接口和关键业务有关,所以安全性很重要。安全措施大体来看主要在两个方面,一方面就是如何保证数据在传输过程中的安全性,另一个方面是数据已经到达服务器端,服务器端如何识别数据,以及如何不被攻击。”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:1,“ normalizeStart”:1},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:1,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”数据加密”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”数据在传输过程中是很容易被抓包的,如果传输通过 http 协议,那么用户传输的数据可以被任何人获取;所以必须对数据加密,常见的做法对关键字段加密,比如用户密码直接通过 MD5 加密;另外一种主流的做法是使用 https 协议,在 http 和 tcp 之间添加一层加密层(SSL层),这一层负责数据的加密和解密。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:2,“ normalizeStart”:2},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:2,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”数据加签”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”数据加签就是由发送者产生一段无法伪造的一段数字串,来保证数据在传输过程中不被篡改,服务端通过验证这个签名来判断是否合法请求。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:3,“ normalizeStart”:3},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:3,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”时间戳机制”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”经过如上的加密,加签处理,就算拿到数据也不能看到真实的数据,但是有不法者直接拿到抓取的数据包进行恶意请求;这时候可以使用时间戳机制,在每次请求中加入当前的时间,服务器端会拿到当前时间和消息中的时间验证,看看是否在一个固定的时间范围内,这样恶意请求的数据包是无法更改里面时间的,所以超过这个时间范围就视为非法请求。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:4,“ normalizeStart”:4},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:4,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”AppId 机制”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”对外提供的接口其实也需要一种机制,并不是谁都可以调用,需要使用接口的用户需要在后台开通 AppId,提供给用户相关的密钥,在调用的接口中需要提供 AppId + 密钥,服务器端会进行相关的验证,获取接口调用凭据 access_token。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:5,“ normalizeStart”:5},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:5,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”限流机制”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”本来就是真实的用户,并且开通了 AppId,但是出现频繁调用接口的情况,这种情况需要给相关 AppId 限流处理,常用的限流算法有令牌桶和漏桶算法。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:6,“ normalizeStart”:6},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:6,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”黑名单机制”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”如果此 AppId 进行过很多非法操作,经过分析之后直接将此 AppId 列入黑名单,所有请求直接返回错误码。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ numberedlist” ,“ attrs”:{“ start”:7,“ normalizeStart”:7},“ content”:[{“type”:”listitem”,”content”:[{“type”:”paragraph”,”attrs”:{“indent”:0,”number”:7,”align”:null,”origin”:null},”content”:[{“type”:”text”,”text”:”数据合法性校验”}]}]}]},{“ type”:“ blockquote”,“ content”:[{“type”:”paragraph”,”content”:[{“type”:”text”,”text”:”只有在数据是合法的情况下才会进行数据处理;每个接口都要有验证规则,当然也可能有一些常规性的规则,比如身份证长度和组成,电话号码长度和组成等等。”}]}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}},{“ type”:“ heading” ,“ attrs”:{“ align”:null,“ level”:3},“ content”:[{“type”:”text”,”text”:”总结”}]},{“ 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 设计常见的原则:标准化、兼容性、抽象性、简单性、高性能、幂等性、安全性,其中安全措施机制包括:数据加密、数据加签、时间戳机制、AppId 机制、限流机制、黑名单机制以及数据合法性校验。当然肯定有其他方式,欢迎补充。”}]},{“ 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://yezhwi.github.io/”}]},{“ type”:“ paragraph”,“ attrs”:{“ indent”:0,“ number”:0,“ align”:null,“ origin”:null}}]}