# 数据安全（反爬虫）之「防重放」策略

> 在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中讲了 Web 前端和 Native 客户端如何从数据安全层面做反爬虫策略，本文接着之前的背景，将从 **API 数据接口**的层面讲一种技术方案，实现数据安全。




## 一、 API 接口请求安全性问题

API 接口存在很多常见的安全性问题，常见的有下面几种情况
1. 即使采用 HTTPS，诸如 Charles、Wireshark 之类的专业抓包工具可以扮演证书颁发、校验的角色，因此可以查看到数据
2. 拿到请求信息后原封不动的发起第二个请求，在服务器上生产了部分脏数据（接口是背后的逻辑是对 DB 的数据插入、删除等）


所以针对上述的问题也有一些解决方案：
1. HTTPS 证书的双向认证解决抓包工具问题
2. 假如通过网络层高手截获了 HTTPS 加证书认证后的数据，所以需要对请求参数做签名
2. 「防重放策略」解决请求的多次发起问题

关于 HTTPS 证书双向认证和 Web 端反爬虫技术方案均在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中有具体讲解。接下来引出本文主角：防重放




## 二、 请求参数防篡改

在之前的文章也讲过，HTTPS 依旧可以被抓包，造成安全问题。抓包工具下数据依旧是裸奔的，可以查看[Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md)文中讲的如何获取 HTTPS 数据。

假如通过网络层高手截获了 HTTPS 加证书认证后的数据，所以需要对请求参数做签名。步骤如下
- 客户端使用约定好的密钥对请求参数进行加密，得到签名 signature。并将签名加入到请求参数中，发送给服务端
- 服务端接收到客户端请求，使用约定好的密钥对请求参数（不包括 signature）进行再次签名，得到值 autograph
- 服务器对比 signature 和 autograph，相等则认为是一次合法请求，否则则认为参数被篡改，判定为一次非法请求

因为中间人不知道签名密钥，所以即使拦截到请求，修改了某项参数，但是无法得到正确的签名 signature，这样构造的一个请求，会被服务器判定为一次非法请求。




## 三、 防重放策略

在工程师文化中，我们要做一个事情，就首先要对这个事情下个定义。我们才能知道做什么、怎么做。

理论上，一个 API 接口请求被收到，服务会做校验，但是当一个合法请求被中间人拦截后，中间人原封不动得重复发送该请求一次或多次，这种重复利用合法请求进行得攻击被成为**重放**。

重放会造成服务器问题，所以我们需要针对重放做防重放。本质上就是如何区别去一次正常、合法的请求。



### 3.1 基于 timestamp 的方案

理论上，客户端发起一次请求，到服务端接收到这个请求的时间，业界判定为不超过60秒。利用这个特征，客户端每次请求都加上 timestamp1，客户端将 timestamp1 和其他请求参数一起签名得到 signature，之后发送请求到服务器。

- 服务器拿到当前时间戳 timestamp2，timestap2 - timestamp1 > 60s，则认为非法
- 服务端接收到客户端请求，使用约定好的密钥对请求参数（不包括 signature、timestamp1）进行再次签名，得到值 autograph。比对 signature 和 autograph，若不相等则认为是一次非法请求

假如中间人拦截到请求，修改了 timestamp 或者其他的任何参数，但是不知道密钥，所以服务器依旧判定为非法请求。
中间人从抓包、篡改参数、发起请求的过程一般来说大于60秒，所以服务器依旧会判定为非法请求。

基于 timestamp 的设计缺陷也很明显，种种原因下，60秒内的请求，会钻规则漏洞，服务器判定为一次合法请求。



### 3.2 基于 nonce 的方案

既然时间戳会有漏洞，那么新方案是基于随机字符串 nonce。也就是说每次请求都加入一个随机字符串，然后将其他参数一起利用密钥加密得到签名 signature。服务端收到请求后
- 先判断 nonce 参数是否能存在于某个集合中，如果存在则认为是非法请求；如果不存在，则将 nonce 添加到当前的集合中
- 服务端将客户端请求参数（除 nonce）结合密钥加密得到 autograph，将 signature 和 autograph 比对，不相等则认为非法请求

但是该方案也有缺点，因为当次的请求都需要和集合中去搜索匹配，所以该集合不能太大，不然匹配算法特别耗时，接口性能降低。所以不得不定期删除部分 nonce 值。但是这样的情况下，被删除的 nonce 被利用为重放攻击，服务器判定为合法请求。

假设服务器只保存24小时内请求的 nonce，该存储仍旧是一笔不小的开销。



### 3.3 基于 timestamp + nonce 的方案

根据 timestamp 和 nonce 各自的特点：timestamp 无法解决60秒内的重放请求；nonce 存储和查找消耗较大。所以结合2者的特点，便有了 「timestamp + nonce 的防重放方案」。

- 利用 timestamp 解决超过60秒被认为非法请求的问题
- 利用 nonce 解决 timestamp 60秒内的漏网之鱼

步骤：

1. 客户端将当前 timestamp1、随机字符串和其他请求参数，按照密钥，生成签名 signature
2. 服务端收到请求，利用服务端密钥，将除 timestamp1、随机字符串之外的请求参数，加密生成签名 autograph
3. 服务端对比 signature 和 autograph，不相等则认为非法请求
4. 拿到服务端时间戳， timestamp2 - timestamp1 < 60，则判定为一次合法请求，然后保存 nonce
5. 服务端只保存60秒内的 nonce，定时将集合内过期的 nonce 删除

该集合不应该直接操作文件或者数据库，否则服务端 IO 太多，造成性能瓶颈。可以是 mmap 或者其他内存到文件的读写机制。根据场景可以选择乐观锁、悲观锁。

其中有一个 timestamp 的问题，服务器会将请求参数中的 timestamp 判断差值，其中一个致命的缺点是服务器的时间和客户端的时间是存在时间差的，当然你也可以通过校验时间戳解决此问题。时间同步请继续看下面部分。




## 四、 计算机网络时间同步技术原理

客户端和服务端的时间同步在很多场景下非常重要，比如秒杀系统（页面打开，各个类目的商品展示倒计时秒杀功能。如果直接请求接口页面数据，然后拿到服务器时间进行倒计时，则会因为网络传输的耗时，导致时间不精确）、接口 timestamp 等。

1. 如果精度要求不高的情况下：先请求服务器上的时间 ServerTime，然后记录下来，同时记录当前的时间 LocalTime1；需要获取当前的时间时，用最新的当前时间 (LocalTime2 - LocalTime1 + ServerTime)

    拿 iOS 端举例：
    - App 启动后通过接口获取服务器时间 ServerTime，保存本地。并同时记录当前时间 LocalTime1
    - 需要使用服务器时间时，先拿到当前时间 LocalTime2 - LocalTime1 + ServerTime
    - 若获取服务器时间接口失败，则从缓存中拿到之前同步的结果（初始的时间在 App 打包阶段内置了）
    - 使用 `NSSystemClockDidChangeNotification` 监测系统时间发生改变，若变化则重新获取接口，进行时同步


2. 如果需要精度更高，比如 100纳秒的情况，则需要使用 NTP（Network Time Protocol）网络时间协议、PTP （Precision Time Protocol）精确时间同步协议了。

NTP、PTP 不在本文的范畴，不懂得可以查看这篇[文章](https://segmentfault.com/a/1190000005337116)
