API 接口设计最佳实践
前言
最近团队内部在做故障复盘的时候发现有很多故障都是因为接口设计不当导致的,这里我就整理归纳一下在接口设计层面需要注意的地方。
API 接口设计
Token 设计
Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。
Token 的值一般用 UUID(算法比较著名的有雪花算法),当服务端接收到客户端请求后会生成 Token(一串字符,如 etye0fgkgk4ca2ttdsl0ae9a5dd77471fgf),然后将 Token 作为 key 将一些和 Token 关联的信息作为 value 保存到如 Redis 缓存数据库中,同步把该 Token 返回给客户端;后续该客户端的请求都需要带上这个 Token,服务器收到请求后就会去缓存服务器中匹配这个 Token 是否存在,存在则调用接口,不存在返回接口错误。
Token 种类
API Token(接口令牌): 一般用于访问不需要用户登录的接口,如登录、注册、一些基本数据的获取(如信用卡官网的如信用卡费率相关信息)等。获取接口令牌需要拿 appId、timestamp 和 sign 来换;其中该 sign 值一般是把 timestamp、key 和对应的参数先进行字母排序再进行 MD5 加密(有时候会加盐),即 sign=MD5(排序(timestamp+key+参数));
假设 API 的请求参数为 channel:T,discount:90%,quantities:10,根据参数名称的 ASCII 码表的顺序排序即为:channel:T,discount:90%,quantities:10。
接着把排序后的参数名和参数值拼装在一起为:channelTdiscount90%quantities10。
把拼装好的字符串采用 utf-8 编码,使用签名算法对编码后的字节流进行摘要,即为 sign=md5(channelTdiscount90%quantities10);
最后,Token=hex(appid+sign+timestamp+salt),即可获得十六进制的一串字符,如“68656C6C6F776F726C64”。
USER Token(用户令牌): 用于访问需要用户登录之后的接口,如:获取我的基本信息、保存、修改、删除等操作。获取用户令牌需要拿用户名和密码来换。
API 接口设计原则
1、明确协议规范
在设计初期需要明确双方的通讯协议是 TCP、HTTP、RPC,一般针对比较敏感的交易或者行业(如金融业),建议使用 HTTPS 协议以确保数据交互的安全。
2、统一接口路径规范
建议采用 Restful 的风格,一般采用这样的格式:控制器名/方法名。具体请参考以下例子:
POST /recommend/cardlist
3、统一接口版本管理
APP 后台逻辑总是处于变化当中,但是 APP 端(如安卓和 ios)因为涉及到应用市场的审核问题,还有这些 2C 端的 APP 应用存在版本碎片化的问题,因此后台暴露的接口需要在一段时间内支持不同版本的接口,一般方法是通过 Nginx 通过配置过滤根据接口的不同版本进行路由分发。
一般来说,接口的版本管理一般有以下两种方法:
-
在 URL 中加入 version 信息,如下述;
-
在 HTTP header 加入 version 信息,这样就等于只有一个接口,但是具体的不同版本的业务逻辑由后台区分处理。
POST v1/recommend/cardlist
Nginx的路由分发:
server {
listen 80;
server_name vip.com;
location /v1/ {
proxy_pass http://129.0.0.1:8001/;
proxy_redirect http://129.0.0.1:8001/ /v1/;
proxysetheader Host $host;
}
location /v2/ {
proxy_pass http://129.0.0.1:8002/;
proxy_redirect http://129.0.0.1:8002/ /v2/;
proxysetheader Host $host;
}
}
server {
listen 8001;
allow 129.0.0.1;
deney all;
server_name vip.com;
root vip.com/v1/;
}
server {
listen 8002;
allow 129.0.0.1;
deney all;
server_name vip.com;
root vip.com/v2/;
}
4、为你的接口设定调用门槛
为调用你的系统分配一个 ID 和 key,针对每个请求对 ID 和 key 进行校验,避免在企业内网中的其他系统只要知道接口被可以随意调用。
5、接口返回规范
返回数据尽量统一规范,务必包括:返回码、返回信息、数据。
{
"code" : 0,
"content" : "string", <- 这里为 JSON
"message" : "string"
}
6、接口安全规范
当我们开发的接口需要暴露到公网,这样的风险跟我们在企业内网暴露给其他系统调用的风险是不可同日而语的。其中有很多风险需要我们一一解决。以下仅提供能想到的:
6.1.数据如何防止被看到?
目前业界老生常谈就是对称加密和非对称加密。
对称加密:对称密钥在加密和解密的过程中使用的密钥是相同的,常见的对称加密算法有 DES,AES;优点是计算速度快,缺点是在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥,如果一方的秘钥被泄露,那么加密信息也就不安全了;
非对称加密:服务端会生成一对密钥,私钥存放在服务器端,公钥可以发布给任何人使用;优点就是比起对称加密更加安全,但是加解密的速度比对称加密慢太多了;广泛使用的是 RSA 算法;
目前主流的做法是在传输层使用 https 协议,http 和 tcp 之间添加一层加密层(SSL 层),这一层负责数据的加密和解密。https 协议则是巧妙的利用上述两种对称加密方法;浅显一点说就是客户端和服务端建立三次握手连接过程中通过交换双方非对称公钥,接着使用对方非对称公钥加密双方约定好的对称密钥,这样就只有双方有这个对称密钥(这样的非对称加密可以保证很安全的把对称密钥给到对方)。后续双方的报文沟通就可以使用该对称密钥进行加解密(这样的对称加密可以保证请求报文可以快速被解密处理,并在处理后被快速加密响应回去)。
6.2.数据如何防止给篡改?
这个时候我们需要对数据进行加签,数据签名平时用得比较多的是 MD5,即将需要提交的数据通过某种方式组合和一个字符串,然后通过 MD5 生成一段加密字符串,这段加密字符串就是数据包的签名。具体请看以下的图。
6.3.时间戳机制
如果加密数据被抓包后被用于重放攻击,我们怎么办?这个时候我们可以把解密后的 URL 参数中的时间戳与系统时间进行比较,如果时间差超过一定间距(如 5 分钟)即认为该报文被劫持并返回错误。但是,务必保证该时间戳的超时时间一定要跟 sign 保存的有效时间一致。
客户端在第一次访问服务端时,服务端将 sign 缓存到 Redis 中并把有效时间设定为跟时间戳的超时时间一致;如果有人使用同一个 URL 再次访问,如果发现缓存服务器中已经存在了本次的 sign,则拒绝服务;如果在 Redis 中的 sign 失效的情况下,有人使用同一个 URL 再次访问,则会被时间戳超时机制拦截。这样的话,就可以避免 URL 被别人截获后的重放攻击。
整个流程如下:
1、客户端通过用户名密码登录服务器并获取 Token
2、客户端生成时间戳 timestamp,并将 timestamp 作为其中一个参数
3、客户端将所有的参数,包括 Token 和 timestamp 按照自己的算法进行排序加密得到签名 sign
4、将 token、timestamp 和 sign 作为请求时必须携带的参数加在每个请求的 URL 后边(http://url/request?token=123×tamp=123&sign=123123123)
5、服务端写一个过滤器对 token、timestamp 和 sign 进行验证,只有在 token 有效、timestamp 未超时、缓存服务器中不存在 sign 三种情况同时满足,本次请求才有效。
6.4.随机数机制
另外,一般会在 URL 参数上加上随机数(即所谓的加盐)并与 6.3 的时间戳机制组合使用以便提升防重复提交攻击。
6.5.黑名单机制
针对同一个 IP 在短时间内频繁请求的,可以通过 Nginx 进行过滤,同步可以在 Nginx 部署动态黑名单(即 IP 实时更新到黑名单库),这样可以防控少量的 DDOS。但受限于判断黑名单需要考虑多维度的信息,一般我们的 Nginx 尽量只做同一 IP 校验,更多维度的黑名单校验可以通过厂商去解决。
6.6.数据合法性校验
这里的数据合法性校验主要指的是数据格式校验和业务规则校验。
数据格式校验:日期格式校验、长度校验、非空校验等;
业务规则校验:如库存校验、身份证合法性校验等。
7、幂等性
定义:在计算机中,表示对同一个过程应用相同的参数多次和应用一次产生的效果是一样,这样的过程即被称为满足幂等性。
具体的解决方案有 token 机制、分布式锁、状态机等方案;这里引用一下之前看到的一篇文章,写得比较详细: https://blog.csdn.net/u011635492/article/details/81058153
8、接口设计的一些最佳实践
-
即使返回的 JSON 中某字段没有值(即空值),也一定要返回该字段。同时前端也需做好这类情况的容错处理;
-
针对单页面的多接口请求,为避免扩大攻击面,建议把多接口逻辑整合到一个接口,一个页面直接调用该接口,以避免绕过部分接口进行攻击;
-
接口最好支持分页;分页一般有电梯式分页(即一开始算好总页数,优劣也一目了然)和游标式分页(即每次查询会拿上一页的最大的那个 ID 即 cursor 进行查询,这种方式更适合类似以时间为排序条件的互联网单页应用);
-
针对你的接口提前做好限流;一般常用的限流有计数器、令牌桶、漏桶这三种。具体请参考接口中的几种限流实现。
API 接口管理
一家公司的每个系统都会有各种各样的接口,但是大部分公司,特别是传统行业的公司的所谓接口文档更多是当每个系传统的 word 文本格式,这种传统的格式有着人尽皆知的痛点:
-
维护不及时;
-
与代码不同步;
-
归档后“便束之高阁”;
-
接口文档跟代码没有互动;
-
文本检索无法建立全局搜索,需要额外借助工具。
为了解决上述的问题,需要建立一套行之有效的接口管理体系,该体系的目标是:
-
能够进行接口文档管理,作为后续的接口治理的其中一部分;
-
能作为接口测试的平台,这样能保证接口跟代码是同步的;
-
支持文本检索。
业界有很多不同的 API 接口管理平台。如去哪儿网的 YAPI 平台、阿里某团队开发的 RAP 平台、Swagger、easyAPI。目前个人在试用 YAPI 平台,后面补充具体的教程与使用体验吧。