http接口签名机制
http接口的内容是明文传输,为了提高传输过程参数的防篡改性,签名sign的方式是目前比较常用的方式。例如腾讯开放平台api
https://wiki.open.qq.com/wiki/%E8%85%BE%E8%AE%AF%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%94%E7%94%A8%E7%AD%BE%E5%90%8D%E5%8F%82%E6%95%B0sig%E7%9A%84%E8%AF%B4%E6%98%8E
首先给需要调用我们开放接口的开发者分发appId(开发者标识,确保唯一)和appSecret(用于接口加密,确保不易被穷举,生成算法不易被猜测)
签名过程:
- 将所有参数(注意是所有参数),除去sign本身,以及值是空的参数,按参数名字母升序排序。
- 然后把排序后的参数按参数1值1参数2值2…参数n值n(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将"转成”后再拼接)的方式拼接成一个字符串。
- 把分配给接入方的验证密钥key拼接在第2步得到的字符串前面。
第2步: 在上一步得到的字符串前面加上验证密钥(appSecret),然后计算md5值,得到32位字符串,然后转成大写. - 计算第3步字符串的md5值(32位),然后转成大写,得到的字符串作为sign的值。
举例:
假设传输的数据是http://www.xxx.com/interface.aspx?sign=sign_value&p2=v2&p1=v1&method=cancel&p3=&pn=vn(实际情况最好是通过post方式发送),
其中sign参数对应的sign_value就是签名的值。
- 拼接字符串,首先去除sign参数本身,然后去除值是空的参数p3,剩下p2=v2&p1=v1&method=cancel&pn=vn,然后按参数名字符升序排序,method=cancel&p1=v1&p2=v2&pn=vn.
- 然后做参数名和值的拼接,最后得到methodcancelp1v1p2v2pnvn
- 在上面拼接得到的字符串最后面加上验证密钥appSecret,我们假设是abc,得到新的字符串methodcancelp1v1p2v2pnvnabc
- 然后将这个字符串进行md5计算,假设得到的是abcdef,然后转为大写,得到ABCDEF这个值即为sign签名值。
注意,计算md5之前请确保接口与接入方的字符串编码一致,如统一使用utf-8编码或者GBK编码,如果编码方式不一致则计算出来的签名会校验失败。
优点:上面的加签过程如果请求参数被人拿走,不会有问题,其他人永远也拿不到appSecret因为appSecret是不传递的,只用来生成签名。
缺点:但是无法避免有人获得了请求的完整地址,然后继续使用相同的参数来重复请求就可以获取数据,为此我们需要添加时间戳校验来保证请求的唯一性,就是对应请求只能使用一次,这样就算别人拿走了请求的完整链接也是无效的,我们引入了下面的时间戳+nonce方案
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。拥有服务器nonce的原因是为了防止man中间人攻击,以防攻击者捕获有效的服务器响应,并尝试将其重播给客户端
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
在请求参数中,我们加入时间戳 :timestamp,和一个随机数nonce(nonce指唯一的随机字符串,用来标识每个被签名的请求),同样,时间戳和nonce作为请求参数之一,也加入sign算法中进行加密。新请求地址:
http://www.xxx.com/interface.aspx?sign=sign_value&p2=v2&p1=v1&method=cancel&p3=&pn=vn&nonce=v4×tamp=1606103174660
实例
private static final String SIGN_KEY = "signature";
private static final String TIMESTAMP_KEY = "_timestamp";
private static final String APPID_KEY = "appId";
private static final String NONCE_KEY = "nonce";
private static final String REDIS_NONCE_KEY = "nonce_";
private static final String REDIS_APPSESCRET_KEY_ = "appSecret_";
private static final int REQUEST_EXPIRE_TIME = 5 * 60 * 1000;
@Resource
private RedisTemplate redisTemplate;
public R verifySign(HttpServletRequest request) {
try {
/* protected ParameterMap<String, String[]> parameterMap; */
Map<String, String> params = MapUtil.convertMap(request);
String signature = params.get(SIGN_KEY);
String timestamp = params.get(TIMESTAMP_KEY);
String appIdMix = params.get(APPID_KEY);
String paramNonce = params.get(NONCE_KEY);
if (StringUtils.isBlank(signature)
|| StringUtils.isBlank(timestamp) || !StringUtils.isNumeric(timestamp)
|| StringUtils.isBlank(appIdMix) || StringUtils.isBlank(paramNonce))
return R.error(500100, "参数验证异常");
Long appId = Utils.decryptUserId(appIdMix);
String appSecret = (String) redisTemplate.opsForValue().get(REDIS_APPSESCRET_KEY_ + appId);
if (StringUtils.isBlank(appSecret))
return R.error(500101, "未授权的调用来源");
long timestampL = Long.parseLong(timestamp);
if ((System.currentTimeMillis() - timestampL) > REQUEST_EXPIRE_TIME)
return R.error(500102, "请求已超时");
String isMember = (String) redisTemplate.opsForValue().get(REDIS_NONCE_KEY + paramNonce);
if (StringUtils.isNotBlank(isMember))
return R.error(500103, "请求已经被处理, 不可再次请求");
params.remove(SIGN_KEY);
boolean verify = VerifyUtils.verify(params, appSecret, signature);
if (!verify)
return R.error(500104, "签名校验失败");
redisTemplate.opsForValue().set(REDIS_NONCE_KEY + paramNonce, paramNonce,
(System.currentTimeMillis() - timestampL), TimeUnit.MILLISECONDS);
return R.ok();
} catch (Exception e) {
return R.error(e.getMessage());
}
}
网友评论