防御重放攻击的一些思考与线上实践
in with 1 comment

防御重放攻击的一些思考与线上实践

in with 1 comment

防御重放攻击的一些思考与线上实践

重放攻击简单来说就是抓包然后把数据重新再请求一次,相信点进来文章的人都知道是啥就不多啰嗦了。

重放攻击的抵御方法一般都是记录下请求过的数据,在下次请求时拒绝相同的数据。

缓存随机数

{
  "params":"data", // 接口参数
  "nonce":"abcdefg", // 客户端随机生成的随机数
  "sign":"8d777f385d3dfec8815d20f7496026dc", // 签名校验,为了防止请求被篡改做的校验,计算方法:md5(salt + params + nonce + timestamp) 
}

在客户端生成随机数,服务端收到请求后到缓存中去查找这个随机数是否被用过了,随机数在缓存中则直接拒绝请求。

为了避免随机数被篡改造成重放攻击,需要添加签名校验,在检查随机数是否在缓存前需要进行校验,防止参数被篡改。

这个方案有两个问题

随机数 + 时间戳校验

绝大部分的博客都讲到使用 随机数 + 时间戳进行重放攻击的拦截,比如说我们需要对一个接口进行重放攻击拦截,接口设计如下:

{
  "params":"data", // 接口参数
  "nonce":"abcdefg", // 客户端随机生成的随机数
  "timestamp":1631345783405, // 客户端的时间戳
  "sign":"8d777f385d3dfec8815d20f7496026dc", // 签名校验,为了防止请求被篡改做的校验,计算方法:md5(salt + params + nonce + timestamp) 
}

假如说我们只缓存 15 分钟的数据,则服务器所做的工作:

在这样的判断下,
15 分钟内生成的随机数会在缓存中被拒绝,
15 分钟后,缓存内随机数过期了,但是请求会在时间戳校验时被拒绝

有挺多的文章到这里就结束了,但是这个方法存在一个很严重的问题,客户端时间可能和服务器时间差距较大,在这样的设备上请求始终会失败,这显然是不可接受的。

为了解决这个问题,可以在客户端中缓存服务器和本地时间的差值,在校验时间失败时,给客户端返回这样的数据:

{
  "code": 1001, // 错误码
  "timestamp":1631345783405 // 服务器时间戳
}

客户端拿到时间后与本地时间相减,拿到一个时间差,把时间差缓存在本地,后续请求都使用矫正过的时间进行请求。

采用黑名单机制

有时接口需要的响应速度极高,不能接受多余一次缓存查询,而且准确性和及时性要求没有那么高。

比如说账号浏览量统计,可能少量被重放不会出现很大问题,但是不能接受被大量重放。

这时候需要采用黑名单机制,把请求多次的随机数缓存到服务器内存中,再下次请求的时候直接拒绝。

这里给出一个对服务逻辑影响比较小的架构。

image-20210911163046947

如图,正常来说请求日志都会被消息队列收集起来,从消息队列中接出一份日志到 flink 或者一个单独的统计服务中,计算出现了多次的随机数,再把这批随机数名单定期推送到服务器上即可。可以写到数据库中,然后服务器定期拉取。

服务端拉取到被重放多次的随机数后,把数据加载到内存中,每次请求都去查看一下这个随机数是不是已经在黑名单中了,如果是则直接拒绝。

这个方案的优点在于不会每次请求都去查询缓存,响应速度快。缺点就是只能不是很严谨的拦截重放攻击,在第二次重放请求后才会把随机数加入黑名单。