开放平台设计第1篇:接口安全

假如某天我创业了,我们公司具备非常强悍的数据服务能力。

于是我们考虑商业化,将这部分能力通过API的形式对外提供。

这就需要开发一个开放平台。如何设计这样的开放平台呢?首先第一个要处理的就是接口安全问题,而接口安全涉及到两个层面:身份识别和回放攻击。

身份识别

换句话说,怎么知道发请求的人是合法用户(客户)?

在很多文章中都会提到AppID、AppPublicKey、AppSecretKey这三个概念:

AppID:唯一标识一个用户或者客户

AppPublicKey:公钥,可对外暴露

AppSecretKey:私钥,必须严格保密

在绝大多数场景中,只要AppID和AppSecretKey就可以了。下面介绍具体用法:

客户端请求服务端时,需要先生成一个随机数nonce,然后使用私钥(AppSecretKey),对AppID、客户端当前时间戳timestamp、nonce字段生成一个签名signature(sign),然后连同这几个字段一起发给服务端。伪代码如下:

1
2
3
4
5
6
timestamp = Now.CurrentTime()
nonce = Random()
str = "AppID = " + AppID  + " && nonce = " + nonce + " && timestamp = " + timestamp
sign = SHA256(str, AppSecretKey) // 利用私钥对str进行签名
http_request = "https://www.xxx.com/" + str + " && sign = " + sign
get(http_request)

注意,私钥AppSecretKey绝对不应该出现在请求里。

服务端收到请求后,先通过请求参数里的AppID查到对应的AppSecretKey,使用同样的方法,对AppID、timestamp、nonce进行签名,然后判断和请求参数里的sign是否相等。如果相等,身份校验通过,否则校验失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
AppID = GetAppIDFromRequestParams(http_request)
nonce = GetNonceFromRequestParams(http_request)
timestamp = GetTimestampFromRequestParams(http_request)
sign_from_request = GetSignFromRequestParams(http_request)
AppSecretKey = GetAppSecretKeyByAppIDFromDB(AppID)
// 利用这些字段和私钥,生成一个sign
str = "AppID = " + AppID  + " && nonce = " + nonce + " && timestamp = " + timestamp
sign = SHA256(str, AppSecretKey)
if (sign == sign_from_request) {
  // ok 
} else {
  // bad
}

注意事项:

1,生成str时字段的摆放顺序,可以按照对field(AppId、timestamp、nonce)进行字典序排序。只要客户端和服务端都同时约定好就行。

2,timestamp和nonce还有别的用途。下面会继续介绍

3,一般说来,平台给每个客户只分配一个AppID,但是一个客户可以有多个AppSecretKey,不同的AppSecretKey用于不同的接口或者对应不同的权限。如果是这种情况,那么客户端发请求的时候需要在参数里把对应的AppPublicKey带上(仍然使用AppSecretKey进行签名),服务端收到请求后,通过AppID + AppPublicKey唯一定位对应的AppSecretKey。

回放攻击

使用上面的方案,身份识别的问题是解决了,但是还是存在回放攻击的问题:攻击者拿到链接后,重复给服务端发请求。如何解决呢?

这个本质上是请求判重问题。解决方法如下:

1,服务端收到请求后,拿出timestamp,和服务端当前时间进行比较,如果超出一个阈值(比如说十分钟),那拒绝该请求。否则继续。

2,服务端拿出nonce,去db/cache里查询。如果存在,说明请求重复,直接拒绝。如果不存在,正常处理请求,并把nonce写入db/cache。

由于nonce存储量较大,可以考虑设置一个过期时间。如果使用redis来做选型,则可以使用SetNX命令来实现。

有资料说nonce在客户端生成,有资料说nonce应该在服务端生成。个人感觉本质没有差别,都是用来唯一标识请求(request_uuid),而在客户端生成会更加自然一点。