前言
有时候为了保护API,需要用到 API 签名,使用 API 签名的好处:
- 让API只能被特定的人访问
- 防止别人抓包拿到请求参数,通过篡改参数发起新的请求
客户端过程
- 给API调用者分配一个
app_id
和app_secret
,app_secret
调用者和服务端各保存一份,不对外泄露,app_id
需要在调用 API 时作为参数传递。 - 除了
app_id
参数以外,另外增加nonce
、timestamp
参数。timestamp
是时间戳,nonce
是一个随机字符串,每次调用API时都要重新生成,且需要保证唯一性。这两个参数主要用来防止重复请求。 - 将参数列表按字典升序排列,然后在末尾添加上
app_secret
参数,将参数列表拼接成一个字符串,对字符串做md5加密,得到signature签名,最后将签名也传递给API。
将参数列表拼接成字符串,可以有多种实现方法,在本文中我们使用这种方案:
1)将参数列表进行排序,按key字典升序排列
2)在末尾添加上app_secret
参数
3)按key1=value1&key2=value2&key3=value3
的格式,将参数列表拼接成一个字符串,其中value值要经过url编码处理
4)拼接完成后,做md5运算,生成散列值,此值便是签名
服务端过程
- 收到请求后,按照
app_id
参数查询到对应的app_secret
- 判断
timestamp
参数是否已过期 - 判断
nonce
参数是否已经被使用 - 剔除
sign
参数,将其它参数按照约定的签名计算方法,生成一个签名A
,将签名A
与sign
参数进行对比,如果一致则通过 - 将
nonce
参数标记为已使用(可以使用redis来存储,并设置过期时间)
注:
1)时间戳的超时时间不能设置得太小,需要考虑服务端和客户端时间可能会有时差的情况,推荐设置为5分钟
2)对于nonce
参数,redis key的过期时间一定要大于时间戳的过期时间,比如时间戳过期时间是5分钟,那么redis key的过期时间就要 >= 5分钟
PHP代码示例
client.php
<?phpfunction genSignature(array $signArr, $appSecret): string
{ksort($signArr, SORT_STRING);$signArr['app_secret'] = $appSecret;$signStr = http_build_query($signArr);return md5($signStr);
}function buildParams(array $params): array
{$appId = 'xxxx';$appSecret = 'yyyy';$params['app_id'] = $appId;$params['nonce'] = uniqid('', true);$params['timestamp'] = time();$params['sign'] = genSignature($params, $appSecret);return $params;
}$params = ['username' => '小明','gender' => '男',
];
var_dump(buildParams($params));// TODO:使用生成的参数调用API接口
server.php
<?phpfunction signatureVerify(array $signArr, $appSecret): string
{unset($signArr['sign']);ksort($signArr, SORT_STRING);$signArr['app_secret'] = $appSecret;$signStr = http_build_query($signArr);return md5($signStr);
}$params = $_GET;$appId = $params['app_id'];
// TODO:根据app_id参数查找对应的app_secret
$appSecret = 'yyyy';$mySign = signatureVerify($params, $appSecret);
if ($mySign === $params['sign']) {echo '签名验证成功';
} else {echo '签名验证失败';
}// TODO:检查timestamp是否已过期
// TODO:检查nonce是否已被使用过
// TODO:将nonce存储到redis,并设置key的过期时间
关于app_secret的安全性
在上述整个过程中,最重要的是保证app_secret
不外泄,如果app_secret
外泄了,其他人就可以用它来生成签名。
但真实情况下,我们很难保证app_secret
不外泄,如果客户端是server端还好说,但如果客户端是浏览器的话,由于网页代码都是可以被看到的,所以只要查看一遍网页的js代码,就可以找到app_secret
。
如果客户端是安卓APP,别人也可以通过拆解apk安装包,破解拿到app_secret
。
面对这种情况也没有完美的方法,毕竟源代码都被别人看到了,还能有啥安全可言。只能通过js代码混淆、apk包安全加固等方法来提高别人找到app_secret
的难度。
加密
虽然我们给接口加了签名验证,但别人还是可以通过浏览器的开发者工具、或者抓包看到具体传递的参数名和参数值。
如果不想别人看到这些信息的话,可以将参数都加密,加密方法可以使用RSA加密(使用服务端公钥加密,服务端收到请求后使用私钥解密)、或者AES加密。
加密后,别人抓包看到的就都是密文了,可以提高别人破解接口的难度。