接口安全问题
无状态登录、token鉴权属于基础功能不在本文讨论范围
接口安全一般分以下几种:
- 请求的身份是否合法?(能否证明你是你)
- 请求的参数是否被篡改?(参数真实性)
- 请求是否唯一?(一个请求不允许重复使用)
以及部署HTTPS协议基本可以防止大部分安全问题,但是正如我之前所说的只有用矛攻击过才知道怎么用盾防,当然攻击并不是让你非要攻击,之前文章提到的可以使用骇客系统模拟攻击至少能让你知道攻击者如何攻击,知其然更要知其所以然, 否则这里做的一系列安全措施搞不懂那可真是大雾里看天--迷迷糊糊。
AccessKey&SecretKey (开放平台)
此处操作主要保证每次请求都是唯一且有效不被篡改的,一般用于三方接口对接
1. 请求身份
为开发者分配 AccessKey(开发者标识,确保唯一)和 SecretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。
2. 防止篡改()
- 按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
- 在stringA最后拼接上 [ SecretKey ] [ timestamp ] 得到字符串stringSignTemp;
- 对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为小写,得到sign值。
客户端:
- (1)从内容算摘要(哈希算法)
- (2)从摘要明文到摘要密文,也称数字签名(发送方私钥+加密算法) 即:客户端将请求参数按照 key 字典序排序
服务端:
- (1)从摘要密文(数字签名)到摘要明文(发送方公钥+解密算法)
- (2)从收到的内容当中计算摘要(哈希算法),与(1)的结果比对是否一致 即:发送方接收方两者都通过一样的算法和约定好的key SecretKey,提取出摘要,然后比对这两个摘要是否一致来完成验证,客户端对参数签名不可抵赖
请求携带参数AccessKey和Sign,只有拥有合法的身份AccessKey和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。
那么这里的签名算法中为什么要对请求参数按参数名做字典序升序排列?
因为这种对接三方接口时传的参数大部分都是前端传参给后端,后端再传输给三方接口,这中间可能存在前端js处理参数、后端代码逻辑处理又或不同的客户端语言、及客户端语言都有各自的参数封装和解析实现,实现可能不尽相同导致参数顺序被打乱,这就导致客户端生成签名时拼接参数的顺序是无法确定的,所以为了保证客户端生成签名时用的参数顺序和服务器的顺序一致,不出现参数一致却签名对不上的情况,API方(第三方接口)提供一个排序方案,使用者遵循此方案,以此来保证大家保证签名一致。
实践可以参考我之前对接过腾讯下某医疗功能接口: Gist地址
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
/**
* 请求方参数签名
* signature:接口签名
* partnerId:合作方 ID
* openId:用户唯一id
* appId: 应用 ID,对应公众号、小程序或者自定义服务标识
*/
public Result<String> getFullGuideParam() {
log.info("start splicing url");
long timestamp = System.currentTimeMillis();
String openId = SecurityUtils.getCurrentUserLogin();
TreeMap<String,String> params = new TreeMap<>();
params.put("appid",appId);
params.put("openId",openId);
params.put("partnerId",partnerId);
params.put("timestamp",String.valueOf(timestamp));
// key转小写,按照Key排序
Map<String, String> targetTableColumnListMap = params.entrySet().stream().collect(Collectors.toMap(
entry -> entry.getKey().toLowerCase(),
Map.Entry::getValue,
(e1, e2) -> e2,
LinkedHashMap::new));
// map转为url参数 ?a=1&b=2
String partUrlEncrypt = getUrlParamsByMap(targetTableColumnListMap);
String signature;
try {
signature = EncryptUtil.hmacSha256(partnerKey,partUrlEncrypt);
} catch (Exception e) {
log.error("签名加密异常:{}",e.getMessage());
return Result.badRequest(INTERNAL_SERVER_ERROR);
}
params.put("signature",signature);
params.put("loginType","h5");
String urlParamsByMap = getUrlParamsByMap(params);
log.info("签名参数url:{},params:{}",MIYING_URL,urlParamsByMap);
return Result.ok(MIYING_URL+urlParamsByMap);
}
重放攻击
虽然上边解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患,我们就在这里解决这个问题
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在15分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在15分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,如存在已有集合,则拒绝。否则,记录该nonce,并删除集合内时间戳大于15分钟的nonce(可以使用redis的expire,新增nonce的同时设置它的超时失效时间为15分钟)。
代码实现
请求接口: aidu.com/demo?a=Jams&b=Tom&c=Cilrs" rel="external nofollow noopenter" > http://api.baidu.com/demo?a=Jams&b=Tom&c=Cilrs
- 客户端
- 生成timestamp和唯一的随机字符串nonce=${保证唯一的值}
- 按照请求参数的字母升序排列非空请求参数(包含AccessKey)
stringA="AccessKey=access&a=Jams&b=Tom&c=Cilrs×tamp=now&nonce=random";
- 拼接密钥SecretKey
stringSignTemp="AccessKey=access&a=Jams&b=Tom&c=Cilrs×tamp=now&nonce=random&SecretKey=secret";
- MD5并且转换为小写
sign=MD5(stringSignTemp).toLowerCase();
- 最终请求
http://api.baidu.com/demo?a=Jams&b=Tom&c=Cilrs×tamp=now&nonce=nonce&sign=sign;
- 服务端
结语
每一套安全技术方案的背后都有一段让人心力交瘁、折磨不已痛并快乐着的红�����对抗,建议大家没事的时候也可以参考下面几篇文章了解"how",明白原理才是王道。