# 为什么会有接口安全问题?

在探讨接口安全规范前,我们先来分析一下为什么会有接口安全问题?具体有几种?

# http

一个常规 http 的 get 请求:

http://api.xx.com/user/getUserInfo?uid=xxx

该示例使用的是 get 请求方式,参数过于暴露容易成为攻击对象,造成数据泄漏;即使使用 post 方式,最多也是在地址栏上不可见接口和参数而已,在传输角度来说,它们都属于明文传输,通过抓包的方式可以获得数据报文,进而导致数据泄漏;

# https

https 协议在 http 的基础上加了一层 ssl 层,数据是被加密后再传输,使用抓包工具抓到的是密文,因此 https 比 http 更安全;

# 中间人攻击

但 https 也可以被中间人攻击手段来拿到数据明文,达到拦截(泄漏)、篡改数据的目的。

# 数据加密

了解到 https 也不是绝对安全的,我们可以对一些关键的数据进行加密后再传输,如:token;即使通过中间人攻击的方式拿到了数据,但也只能拿到加密后的数据(密文),因此加密数据可以防止数据被 泄漏篡改

那我们加密后的数据有没有可能被破解?这就取决于我们的加密算法了,这里使用的常规加密算法有:对称加密、非对称加密;

  • 对称加密算法:加密解密都是用同一个密钥、效率高、但安全性低,一旦密钥泄漏就很容易被破解;
  • 非对称加密算法:使用密钥对(公钥和私钥)、安全性高、但效率低,公钥可以发给任何请求方(通常是客户端),而私钥不能外泄(通常由服务端保管)。

如果使用对称加密算法,那么客户端和服务端均需要持有相同的密钥来进行加解密,但客户端有被逆向破解的风险,密钥泄漏的概率较高,因此在客户端上持久化或硬编码密钥都不够安全;

如果使用非对称加密算法,那么由客户端持有公钥,服务端持有私钥。请求接口前,客户端先使用公钥加密,再传输,即使被中间人拦截拿到了数据的密文也无法破解,因为只有服务端的私钥才能解密;但这种方式效率低,服务端处理压力大,会降低服务器性能,所以不适合用来处理常规业务的加解密;

以上两种加密算法各有优劣势,而我们的业务场景肯定是期望有一种: 安全高加解密效率高 的方案;

在这里我们可以结合两种加密算法优点采取:RSA(非对称)+AES(对称)的方式进行加解密;

# RSA+AES 方案

该方案中,客户端持有公钥( rsa_pub_key ),服务端持有私钥( rsa_pri_key )。客户端在访问常规业务接口之前,需要先请求一个初始化接口,该接口用于和服务端交换密钥。

具体做法是:客户端每次启动,都认为是一次会话,每次会话都生成一个 sessionKey ,一旦应用被杀死,那么本次会话结束, sessionKey 丢失;下次启动应用,又是一次新的会话,需重新生成 sessionKey ,即:应用生命周期的开始和结束,就是会话( sessionKey )生命周期的开始和结束。

sessionKey 被用来当作 AES 对称性加解密的密钥,即请求初始化接口时,需要使用 rsa_pub_key (公钥)对 sessionKey 进行加密,后台接收到请求后,使用 rsa_pri_key 进行解密,得到 sessionKey

那么到此,客户端和服务端都持有一个用于 AES(对称加密)的密钥: sessionKey ,后续的敏感数据,可以使用 sessionKey 进行加密,然后服务器再使用 sessionKey 进行解密,如:对登录、注册接口的账号、密码进行加密,又如:对银行账户信息进行加密;

伪代码:

// 客户端使用 rsa_pub_key 加密 sessionKey 传给后台
data = rsaEncrypt(sessionKey)
// 服务端使用 ras_pri_key 解密得到 sessionKey
sessionkey = rsaDecrypt(data)

该方案中:只有在应用启动时,进行一次非对称加解密,后续的请求均使用对称性加解密,因此其安全性和效率性能都满足需求;

# 有哪些攻击?

# 数据篡改

数据篡改主要发生在网络节点,中间人攻击是常用的一种方式。在这里假设我们使用的是 https 协议请求接口,但由于该接口不是一个数据敏感的接口,所以没对参数进行加密保护。在这种情况下是可以被拦截拿到明文的,并且可以篡改参数达到非法操作的目的。如:论坛发帖子,被拦截后,中间人可以任意修改帖子内容,再发布,可造成:恶意帖子、不良链接、不良图片、广告等内容的流出;

# 验签

验签是用来防止数据篡改的一种经典手法,原理是:客户端请求时,在 header 中带上一个签名 sign

sign=md5(动态参数串+appSecrectKey)
动态参数串=参数以key-value的格式存储,并以正序(按字母顺序)排序拼接

如:

paramsString="a-aaa;b-bbb;c-ccc;";
sign=md5(paramsString+"secrect-"+appSecrectKey)

其中 appSecrect 可以是硬编码在代码中,也可以是通过初始化接口返回来,目的是为了增加 sign 安全性,提高破解难度。

那么请求的伪代码如下:

request.getHeader.put("sign",sign);

json.put("a", aaa);
json.put("b", bbb);
json.put("c", ccc);

request.setParam(json.toString());
request.execute();

这样后端在接收到请求之后,也按这种约定好的方式计算出 sign 值,如果两个 sign 相等,则认为合法,否则丢弃请求。

如果中间人企图修改其中的参数,如将参数 b 的值改为 zzz,那么后端生成的 sign 就跟客户端传过来的 sign 匹配不上,认为数据不合法,是被篡改过的。

这时候就有人问了,这个 sign 会不会也可以被伪造?答案是可以的。只要 sign 的生成规则被泄漏了,就会有被伪造的风险。

那么怎么来提高 sign 的安全性呢?上面通过加入 appSecrectKey 的方式就是一个例子,即使中间人拿到了我们的所有参数明文,也知道了我们的 sign 生成规则,但他拿不到我们的 appSecrectKey ,自然也无法伪造 sign。

appSecrectKey 会不会被泄漏呢?这就取决于你们对它的保护程度和时效性定义了。

  • 保护程度:如果是将 appSecrectKey 写死在客户端,那么客户端是有被逆向破解的可能,有一定的泄漏风险;如果是通过一个初始化接口返回,那么安全性会更高;
  • 时效性:可以给 appSecrectKey 设定一个实效性,如:仅限于初始化后才生成,生成规则跟 clientIduserId 挂钩;

# 重放攻击

上述验签方法可以防止数据篡改,但黑客拿到密文数据后,原封不动的发起多次请求,还是会造成数据重复,如:下单接口,黑客用相同的密文不停地请求服务器,会导致数据库增加很多订单。而且容易被暴力攻击,造成数据库读写繁忙,甚至爆满,你的 “友商” 可能会这种骚操作来攻击你的后台。

这时候就需要对上面的 sign 进行改进:

sign=md5(paramsString+nonce+timestamp+appSecrectKey)
  • nonce:是唯一流水号,长度可自行设置,如:32 位的 uuid;
  • timestamp:时间戳,用于鉴定请求的时效性; 客户端每次请求后台都要随机生成唯一的 nonce 值,后台收到请求后,将比对 nonce 是否已存在,如果不存在,则该请求是首次访问,是合法的请求;否则为非首次访问,是非法的请求,直接丢弃请求;随着请求越来越多,nonce 的记录也会越来越多,如果每次请求都去查找 nonce,将会给后台增加查询时间,降低后台性能。

所以,可以通过加入一个 timestamp 字段,后台只处理 1 分钟内(可根据需求调整,如:2 分钟、5 分钟都行)的请求,超过 1 分钟就视为过期请求,直接丢弃。只有在 1 分钟内的请求,才会执行 nonce 判断,这样大大的降低后台的 nonce 判断压力。

但是,随着业务的积累,nonce 还是会有爆满的一天,因此后台可以定期去清理超过一定时间的 nonce,如:2 分钟;

# 暴力攻击

假如上述的 sign 生成规则和 appSecrectKey 都暴露了,那么黑客可以伪造 nonce 和生成合法的 sign,这样子,就越过了我们的后台防线,可以发起高密度的暴力攻击。

# IP 黑名单限制

检测到暴力攻击时,如:1 分钟内发起 100 次或者更多(根据业务场景设定阈值),就认为暴力攻击,将其 IP 加入黑名单,直接拒绝该 IP 的请求即可;

# 高防服务器

除此之外,还可以购买具备:防攻击、防病毒的高防服务器