本文由 简悦 SimpRead) 转码, 原文地址 mp.weixin.qq.com)

背景

随着业务的发展,系统架构从单体架构变为面向服务架构,水平分层架构;再变为微服务架构,

服务网格,服务与服务间的交互越来越复杂,如何优雅的设计一个接口,需要考虑哪些方面?特别是对公服务(比如 BFF)需要对外提供公网域名的接口,安全性怎么保证,我整理了我工作以来一些常见的措施以及具体如何去实现:

数据有效性校验

合法性校验包括:常规性校验以及业务校验;常规性校验:包括必填字段校验,长度校验,类型校验,格式校验等;业务校验:根据实际业务而定,比如订单金额不能小于 0 等;

幂等设计

所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的。数据发生改变才需要做幂等,有些接口是天然保证幂等性的。

比如查询接口,有些对数据的修改是一个常量,并且无其他记录和操作,那也可以说是具有幂等性的。其他情况下,所有涉及对数据的修改、状态的变更就都有必要防止重复性操作的发生。通过间接的实现接口的幂等性来防止重复操作所带来的影响。

又比如我们电商比较常见的加减 GMV 同一个消息无论过来多少次结果都应该只加减一次,不然会导致金额错误甚至造成资损。

请求层面: 多次执行的结果是一致的业务层面: 同一个用户不重复下单,商品不超卖,MQ 不重复消费

幂等的本质是分布式锁的问题,分布式锁正常可以通过 redis 或 zookeeper 实现;

在分布式环境下,锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等

安全性

1. 数据加密

我们知道数据在传输过程中是很容易被抓包的,如果直接传输比如 http 协议传输,那么数据在传输的过程中可能被任何人获取。

所以必须对数据进行加密,常见的做法是对敏感数据比如身份证号进行 md5 加密。现在主流的做法是使用 https 协议,在 http 和 tcp 之间添加一层数数据安全层 (SSL 层),这一层负责数据的加密和解密。https 如何配置和使用,大家翻阅我历史文章自行去研究。

对称加密: 密钥在加密过程中和解密过程中是不变的,常见的算法有 DES,AES; 优点是加解密计算速度快;缺点是数据传送前,服务双方必须约定好密钥,如果一方密钥泄露,加密信息也就不安全了。

非对称加密: 密钥成对出现,一个密钥加密之后,由另外一个密钥来解密;私钥放在服务端文件中,公钥可以发布给任何人使用;优点是比对称加密更安全,但是加解密的速度比对称加密慢多了,广泛使用的是 RSA 算法;

https 的实现正好是结合了两种加密方式,整合了双方的优点,在安全性和性能方面都比较好。对称加密和非对称加密的代码实现,jdk 提供了相关的工具类可以直接使用,本文不过多介绍。

2. 数据签名

介绍 3 种数据签名安全策略:摘要 [KEY] , 签名 [证书] , 签名 + 加密 [证书]

安全策略 描述 安全级别
摘要 [Key] 将数据和 Key(自定义契约密码) 组合后进行摘要 安全级别低,契约密钥安全性非常低。在契约密钥安全情况下能基本保障数据的不可篡改性。
签名 [证书] 使用证书和非对称签名算法对数据进行签名 安全级别中,能够保障数据的不可篡改性和不可抵赖性,但是不能保障数据的私密性
签名 - 加密 [证书] 使用证书和非对称算法对数据签名,使用一次一密的密钥和对称算法对数据进行加密 安全级别高,能够保障数据的不可篡改性和不可抵赖性,而且能保障数据的私密性。
  • 机密性 (Confidentiality): 未经许可不许看
  • 完整性 (Integrity) : 不许篡改
  • 可用性 (Availability) : 防止不可用
  • 不可抵赖性 (Non-Repudiation): 用户不能否认其行为

摘要 [KEY] 过程:将需要提交的数据通过某种方式组合成一个字符串,然后通过 md5 生成一段加密字符串,这段字符串就是数据包的签名,比如:

  1. str:参数1={参数1}&参数2={参数2}&……&参数n={参数n}$key={用户密钥};
  2. MD5.encrypt(str);

摘要 [KEY] 原理:Hash 算法不可逆,并且计算结果具有唯一性,在 key 的隐私得到保证的情况下,可以保证完整性摘要 [KEY] 缺陷:key 的隐私性很难保证,明文传输


签名 [证书] 过程:客户端对明文做一个 md5/SHA 计算,对计算后的值通过私钥加密得到密文,客户端将明文和密文发送给服务端,服务端对密文通过公钥解密得到值 A,同时服务端对明文做一个 md5/SHA 计算得到值 B,比较值 A 与值 B,相同得验证通过,能够保障不可篡性和不可抵赖性,但是不能保障数据的私密性(明文传输)大厂是如何设计接口的? - 图1


签名 + 加密 [证书] 过程:客户端生成一个随机字符串,作为 password,然后把这个 password 通过 B 公钥加密生成密文 C,把 A 明文通过 password 加密生成密文 B, 同时把 A 明文做 MD5/SHA 计算后的值通过 A 私钥加密得到签名 D, 把密文 B 和密文 C 和签名 D 发给服务端,服务端通过私钥解密文 C 得到 password,然后通过 password 解密文 B 就可以得到 A 明文,同时签名可以用来验证发送者是不是 A,以及 A 发送的数据有没有被第三方修改过。

可以假设存在一个恶意的一方 X,冒充了 A,发送了密文 B(password 生成),密文 C 服务端收到数据后,仍然可以正常解密得到明文,但是却无法证明这个明文数据是 A 发送的还是恶意用户 B 发送的。签名 D 的含义就是 A 自己签名,服务端可以验证。X 由于没有 A 的私钥,这个签名它无法冒充,会被服务端识别出来。

大厂是如何设计接口的? - 图2加密 - 签名

3. 时间戳机制

数据经过了加密处理,酒店抓取到了数据也看不到真实数据;但是有不法者不关心真实数据,拿到数据后直接进行恶意请求,这个时候简单的做法可以考虑时间戳机制,在每次请求中加入当前时间,服务端会将报文中的时间与系统当前时间做比对,看是否在一个固定的时间范围内比如 5 分钟,恶意伪造的数据是没法更改报文中时间的,超过 5 分钟就可以当作非法请求了。

伪代码如下:

long interval=5*60*1000;//超时时间  
long clientTime=request.getparameter("clientTime");  
long serverTime=System.currentTimeMillis();  
if(serverTime-clientTime>interval){  
    return new Response("超过处理时长")  
}

4. AppId 机制

大部分网站需要用户名和密码才能登陆,这其实是一种安全机制;对应的服务也可以使用这一机制,不是谁都可以调用,调用服务前必须先申请开通一个唯一的 appid,提供相关的密钥,在调用接口时需要提供 appid + 密钥信息,服务端会进行验证。

appid 使用字母,数字,特殊符号等随机生成,生成的唯一 appid 看系统实际要求是否需要全局唯一;不管是否全局唯一最好有以下属性:

趋势递增: 这样在保存数据库的时候,索引的性能更好

信息安全: 随机生成,不要是连续的,容易被发现规律

关于全局唯一 Id 生成的方式常见的有 snowflake 方式等

snowflake

大厂是如何设计接口的? - 图3Xnip2020-11-04_19-31-00

以上示意图描述了一个序列号的二进制组成结构。

第一位不用,恒为 0,即表示正整数;接下来的 41 位表示时间戳,精确到毫秒。为了节约空间,可以将此时间戳定义为距离某个时间点所经历的毫秒数(Java 默认是 1970-01-01 00:00:00)。

再后来的 10 位用来标识工作机器,如果出现了跨 IDC 的情况,可以将这 10 位一分为二,一部分用于标识 IDC,一部分用于标识服务器;最后 12 位是序列号,自增长。

snowflake 的核心思想是 64bit 的合理分配,但不必要严格按照上图所示的分法。如果在机器较少的情况下,可以适当缩短机器 id 的长度,留出来给序列号。

5. 黑名单机制

如果此 appid 进行过很多非法操作,或者说专门有一个中黑系统,经过分析之后直接将此 appid 列入黑名单,所有请求直接返回错误码;

我们可以给每个 appid 设置一个状态比如包括:初始化状态,正常状态,中黑状态,关闭状态等等;或者我们直接通过分布式配置中心,直接保存黑名单列表,每次检查是否在列表中即可;

限流机制

常用的限流算法包括:令牌桶限流漏桶限流计数器限流

  • 令牌桶限流令牌桶算法的原理是系统以一定速率向桶中放入令牌,填满了就丢弃令牌;请求来时会先从桶中取出令牌,如果能取到令牌,则可以继续完成请求,否则等待或者拒绝服务;令牌桶允许一定程度突发流量,只要有令牌就可以处理,支持一次拿多个令牌;
  • 漏桶限流漏桶算法的原理是按照固定常量速率流出请求,流入请求速率任意,当请求数超过桶的容量时,新的请求等待或者拒绝服务;可以看出漏桶算法可以强制限制数据的传输速度;
  • 计数器限流计数器是一种比较简单粗暴的算法,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流;

具体基于以上算法如何实现,Guava 提供了 RateLimiter 工具类基于基于令牌桶算法:

 RateLimiter rateLimiter = RateLimiter.create(5);

以上代码表示一秒钟只允许处理五个并发请求,以上方式只能用在单应用的请求限流,不能进行全局限流;这个时候就需要分布式限流,可以基于 redis+lua 来实现;

总结

其实接口不管是设计还是开发,如果不是特别急的需求大家都可以多一点思考,这样你的系统才会更稳定,上线和测试过程中 bug 更少,而且从个人提升角度来说,多思考总是一件好事。

很多时候大家都在抱怨:哎呀我公司小,我学校差这种环境得不到成长。傻瓜,很多时候高手也是这样走过来的,不过一样的事情每个人的态度不一样,时间久了结果也就不一样了。

好啦,现在大家应该都上班了,我熬夜值班还在大促现场(文章周末写的,现在就写个总结),我是敖丙,你知道的越多,你不知道的越多,我们下期见。