API接口的签名认证

对于开放的api接口,会面临如下几个安全性问题:

  • 请求来源(身份)是否合法?
  • 请求参数是否被篡改?
  • 请求是否唯一?

为了解决这几个问题,常见的一种做法是在接口中使用签名(Signature)认证机制。

过程

  1. 接口提供方给出AccessKeySecretKey
  2. 调用方根据AccessKey和SecretKey以及请求参数,按照一定算法生成签名sign
  3. 接口提供方验证签名

接口调用方

接口调用方(客户端)生成签名的步骤大同小异,根据接口提供方指定的规则即可,比如下面这个步骤:

  1. 所有参数使用UTF-8编码
  2. 将所有业务请求参数按字母先后顺序排序
  3. 参数名称和参数值链接成一个字符串A
  4. 在字符串A的首尾加上SecretKey组成一个新字符串B
  5. 对字符串进行md5加密,并将加密后的byte数组进行BASE64编码,得到签名sign

假设请求的参数为:f=1,b=23,k=33,排序后为b=23,f=1,k=33,参数名和参数值链接后为b23f1k33,首尾加上SecretKey后md5和Base64:

1
Base64(md5(SecretKeykey1value1key2value2...SecretKey))

PS:1)采用utf-8提前对参数进行编码,可以减少客户端编码差异带来的影响,因为服务端通常都是统一使用utf-8来做的。
2)最后还进行了BASE64编码是为了将将摘要内容全部转换为可显字符,并把字符也做为一个接口的参数,因为不可显字符(即md5加密后的byte数组)在网络传输中可能丢失。

上面的签名方法安全有效地解决了参数被篡改身份验证的问题,如果参数被篡改,没事,因为别人无法知道SecretKey,也就无法重新生成新的sign。

如何解决请求的唯一性:timestamp+nonce方案

使用上面的签名方法可以保证来源及请求参数的合法性,但是请求链接一旦泄露,别人可以拿这个链接反复请求,所以还存在着被盗用二次请求的隐患。
常用的一种做法是在请求参数中带上一个时间戳timestamp,并且把时间戳也作为签名的一部分,在接口提供方对时间戳进行验证,只允许一定时间范围内的请求,例如1分钟。因为请求方和接口提供方的服务器可能存在一定的时间误差,建议时间戳误差在5分钟内比较合适。这样,如果别人没有及时拿到这个链接,就没法啊二次请求。允许的时间误差越大,链接的有效期就越长,请求唯一性的保证就越弱。所以需要在两者之间衡量。
如果要做到更安全,除了时间戳,还带上一个唯一地随机字符串nonce,比如说一个uuid,为每个请求提供一个唯一的标识符。接口提供方可以使用redis的过期机制,将这个nonce存在redis,如果timestamp是在有效期(比如5分钟)内,参数上的nonce已经有过请求记录,那么可以判断为二次请求,直接拒绝。

服务端

接口提供方需要做的过程如下:

  1. 拦截请求,获取所有参数,包括AccessKey,timestamp,nonce,sign等。
  2. 判断timestamp是否在有效范围。
  3. 根据AccessKey查询对应的SecretKey,用客户端相同的加密过程,得到sign,并验证是否与请求参数的sign一致。
  4. 判断nonce在timestamp的有效时间内是否重复,重复则拒绝请求,没有重复则记录当前nonce和timestamp。

示例代码

如下是一个生成sign的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static String buildMD5Sign(Map<String, String> params, String appSecret) {
Map<String, String> paramsMap;
if (params instanceof TreeMap) {
paramsMap = params;
} else {
paramsMap = new TreeMap<>(params);//加入所有参数
}
//拼参数字符串
StringBuilder query = new StringBuilder();
for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
query.append(entry.getKey()).append(entry.getValue());
}

//前后加上secretKey
String temp = appSecret + query.toString() + appSecret;
return DigestUtils.md5Hex(temp).toUpperCase();
}

------ 本文完 ------