开放接口安全那点事


很多系统很多时候需要对外提供一些开放API,这些API不用登录即可调用,为了确保系统安全,我们需要采取一些措施去防范而已请求,以下内容为我在设计开放接口鉴权机制时的记录。

以开放查询数据的接口为例,接口根据id查询详细信息,接口名约定为/api/v1/detail。那么,没有鉴权的请求方式为:/api/v1/detail?id=123(以下均以123为例)

最简单的鉴权

最简单的鉴权方案就是使用一个Token(或者说是密码)进行鉴权。

客户端和服务端约定一个Token,服务端收到请求后判断Token是否和当初的约定一致,一致则执行请求,否则返回错误信息。

现在我们的请求参数是这样的:/api/v1/detail?id=123&token=this_is_a_token

这种方式并不安全,因为Token很容易被抓包抓取到,四舍五入相当于接口在裸奔,而且无法细粒度限制接口的权限(可以查询全部信息)。

升级版鉴权

在上面的鉴权方式的基础上进行升级,在引入一个ak(app key),顾名思义,看名字也能猜出是用来标识调用方的,与之对应的,还要有sk(secret key),即ak对应的密钥,有了这俩,我们就可以得知是谁在调用接口,进而可以针对不同的调用者进行权限限制。

那么,怎办使用这两个呢?明文传输?这当然不行,明文传输依然可以被抓包抓到,四舍五入还是相当于裸奔。那既然明文不行,就密文吧,密文传输虽然不会导致密钥泄漏,但让存在被破解的可能,特别是攻击者得知加密算法之后,被破解的风险更大。

假设现在ak=aaa,sk=bbb,使用MD5算法对sk加密,那么现在我们的请求是这个样子:

/api/v1/detail?id=123&ak=aaa&sk=08f8e0260c64418510cefb2b06eee5cd

此外,通过抓包,依然可以拿到完整的请求参数,如果不修改请求参数,那么这个链接可以访问无数次,仍存在安全风险。

再做点小升级

上面的鉴权方案已经初步具有安全性了,下面针对上面存在的接口无限访问的问题做点优化。

既然不想让一套参数被重复使用无数次,那我们对接口做个幂等,在请求的时候带个没有意义的随机生成的字符串,每次记录一下这个字符串,如果和之前请求相同不执行操作就可以了。

现在,我们的接口长这样:/api/v1/detail?id=123&nonstr=abcde&ak=aaa&sk= 08f8e0260c64418510cefb2b06eee5cd

虽然这样我们解决了重复请求的问题,但又有新问题了,如果我们生成了一样的字符串,那不就没发成功请求了,而且把每次请求的随机字符串都保存下来,那我们服务器的内存肯定扛不住。

给缓存加个过期时间

既然我们缓存随机字符串会消耗内存,那我们给缓存的数据设置一个过期时间不就好了,这样到期就从内存中清除了,不用担心内存消耗完。

现在,我们的请求长这样:/api/v1/detail?id=123&nonstr=abcde&ak=aaa&sk= 08f8e0260c64418510cefb2b06eee5cd&timestamp=1660292467843

可这样问题又来了,如果有攻击者发现了这个特性,每次请求完成后,等一段时间再重新请求,这时候服务器里的随机字符串已经过期,那这个请求就被识别为全新的请求,还是会造成无限次重复使用。

既然是缓存过期时间造成的新问题,那咱们就在参数中引入时间戳,把请求时间一起传给服务器,这样就能根据时间判断请求是否过期,但这样仍然存在风险,攻击者可以伪造时间戳和随机字符串来恶意请求。

最后的优化

经过上面几次修改,接口已经基本具有安全性了,但攻击者仍可通过伪造时间戳来实现而已请求,既然我们不希望参数被伪造,那给参数列表签个名好了,签名算法就用MD5吧。

先来看下接口的样子吧:/api/v1/detail?id=1&ak=aaa&timestamp=1660292467843&sign=74bb50923e3f895e407134f221e5f4cf

这次,我们去掉了nonstr,并将参数里的sk替换为sign,因为sign具有唯一性,所以做幂等校验的时候,使用sign作为标识即可。

那么sk就没有用武之地了吗?当然不是,如果签名算法泄漏,那么请求还是很容易伪造,所以,把参数按照一定格式拼接,再把sk也拼接到一块,最后使用加密算法进行签名。这样即使签名算法泄漏了,只要sk没有泄漏,那么就没法伪造签名。

为了确保服务端、客户端签名一致,在拼接参数时,最好按照字典序排序,确保拼接顺序一致

示例代码:先鸽了,后面再放。

声明:迟於|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 开放接口安全那点事


栖迟於一丘