1 开放接口
- appid和appSecret:为每个应用接入方分配,appid是唯一确定某个应用,appSecret是long-term key,故而为了安全需要可以销毁重新生成。双向认证这里介绍了long-term key是什么。
- 时间戳
客户端接口请求添加时间戳
//增加时间戳参数
params.put("timestamp", DateUtils.formatDate(new Date()));
服务器校验时间戳的有效性,有效期为1分钟。这个1分钟是接口接口到服务器接收到请求的时间,
@Value("${open.sign.timeout:60}")
private long signTimeout;
private boolean checkTimestamp(long timestamp, boolean verifyTimestamp) {
if (!verifyTimestamp) {
return true;
}
Long ct = System.currentTimeMillis() / 1000;
return ct < timestamp + signTimeout;
}
- 随机数
随机数的目的是防止重放攻击,随机数与时间戳一般是一起使用的,API接口防止参数篡改和重放攻击,如果使用fiddler获取到报文,然后进行写个脚本进行重放攻击,60s足够压垮服务器了,故而再增加一次性的nonce,那么重放攻击就可以有效防范了。
接口请求的随机数,例如下面的做法是通过uuid来产生的
这个是接口服务端的代码,从下图可以看到如果随机数在redis中存在,则提示异常,params.put("nonce".randomUUID().toString().replace("-", ""));
/** * 检查随机数 * @param nonce */ private void checkNonce(String nonce) { if (redisService.hasKey(nonce)) { throw new RuntimeException("随机数验证失败"); } else { redisService.putString(nonce, "", signTimeout); } }
- 签名
这里参考了支付宝的签名策略,先对接口参数进行排序,然后再签名。签名后可以规避数据篡改的问题,你并不知道公私钥是什么
private static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
logger.debug("签名数据:{}", prestr);
return prestr;
}
客户端使用私钥签名
public static String sign(String text, String signType, String key) {
String mySign = null;
if (SignType.MD5.equals(signType)) {
mySign = DigestsUtil.md5Hex(text + key);
} else if (SignType.RSA.equals(signType)) {
mySign = RSASignature.signSHA1(text, key);
} else if (SignType.RSA2.equals(signType)) {
mySign = RSASignature.signSHA256(text, key);
} else {
logger.error("不支持的签名方法:{}", signType);
throw new RuntimeException("签名方法不支持");
}
logger.debug("签名:{}", mySign);
return mySign;
}
服务器端用公钥验签
public static boolean verify(Map<String, String> params, String key) {
String sign = null;
if (params.get("sign") != null) {
sign = params.get("sign");
logger.debug("request sign:{}", sign);
} else {
throw new RuntimeException("签名不能为空");
}
String signType = getSignType(params);
// 过滤
Map<String, String> filterParams = paraFilter(params);
// 排序
String linkString = createLinkString(filterParams);
if (SignType.RSA2.equals(signType)) {
return RSASignature.verifySHA256(linkString, sign, key);
} else if (SignType.RSA.equals(signType)) {
return RSASignature.verifySHA1(linkString, sign, key);
} else if (SignType.MD5.equals(signType)) {
String mySign = sign(linkString, signType, key);
return sign.equals(mySign);
} else {
logger.error("不支持的签名方法:{}", signType);
throw new RuntimeException("签名方法不支持");
}
}
- 令牌
2 API网关
3 安全认证
Oauth2.0,参考springsecurity oauth2.0
4 https
DV EV OV证书,在服务端nginx中配置dv证书即可
5 开放平台
5.1 企查查
企查查开放平台接口示例,企查查的开放接口做的比较简单,它为企业用户提供了appId(它的key)和appSecret(下面的SecretKey),token生成方式相对固定,有效期是根据时间戳来保证了,如果在有效时间期内进行重放,影响会影响到它的调用次数,但是对于它这种按次收费的公司,这些调用次数跟他好像没有多大关系,如果这么去看待,这个设计确实有些不负责任。当然前提是你得有SecretKey。
我们公司曾经对接过企查查,也遇到过安全问题,不知道secretkey是怎么泄露的,倒是被错误的计费。SecretKey毕竟是long-term key,时间戳和key都是参数,应该花时间还是可以破解出来的,虽然我不是这方面的专家
5.2 柠檬云
获取平台token,通过appId和appSecret获取access_token,显然必须使用https,否则appSecret一下子就暴露了。access_token的有效期为2小时,在快过期再次获取access_token,这里使用的oauth2.0协议,刷新token并没有使用refresh_token,做了一定裁剪。
5.3 支付宝
5.4 微信