文章目录
- 【如何设计安全可靠的开放接口】系列
- 前言
- timestamp
- nonce
- 白名单机制
- 黑名单机制
- 限流、熔断、降级
- 其他合法性校验
【如何设计安全可靠的开放接口】系列
1. 如何设计安全可靠的开放接口—之Token2. 如何设计安全可靠的开放接口—之AppId、AppSecret3. 如何设计安全可靠的开放接口—之签名(sign)4. 如何设计安全可靠的开放接口【番外篇】—关于MD5应用的介绍5. 如何设计安全可靠的开放接口—还有哪些安全保护措施6. 如何设计安全可靠的开放接口—对请求参加密保护7. 如何设计安全可靠的开放接口【番外篇】— 对称加密算法
前言
上一篇我们通过签名机制完成了身份确认,数据防篡改这两项非常重要的工作,不过如果仅仅如此是不能满足一个安全可靠的接口要求的,本节我们接着来看看,还有哪些是我们还需要补充的。
timestamp
前面在接口设计中,我们使用到了timestamp,这个参数主要可以用来防止同一个请求参数被无限期的使用。
稍微修改一下原服务端校验逻辑,增加了5分钟有效期的校验逻辑。
private static void serverVerify(String requestParam) throws Exception {
APIRequestEntity apiRequestEntity = JSONObject.parseObject(requestParam, APIRequestEntity.class);
Header header = apiRequestEntity.getHeader();
UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);
// 首先,拿到参数后同样进行签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
if (!sign.equals(header.getSign())) {
throw new Exception("数据签名错误!");
}
// 从header中获取相关信息,其中appSecret需要自己根据传过来的appId来获取
String appId = header.getAppId();
String appSecret = getAppSecret(appId);
String nonce = header.getNonce();
String timestamp = header.getTimestamp();
// 请求时间有效期校验
long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if ((now - Long.parseLong(timestamp)) / 1000 / 60 >= 5) {
throw new Exception("请求过期!");
}
cache.put(appId + "_" + nonce, "1");
// 按照同样的方式生成appSign,然后使用公钥进行验签
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[0]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {
throw new Exception("验签错误!");
}
System.out.println();
System.out.println("【提供方】验证通过!");
}
nonce
nonce值是一个由接口请求方生成的随机数,在有需要的场景中,可以用它来实现请求一次性有效,也就是说同样的请求参数只能使用一次,这样可以避免接口重放攻击。
具体实现方式:接口请求方每次请求都会随机生成一个不重复的nonce值,接口提供方可以使用一个存储容器(为了方便演示,我使用的是guava提供的本地缓存,生产环境中可以使用redis这样的分布式存储方式),每次先在容器中看看是否存在接口请求方发来的nonce值,如果不存在则表明是第一次请求,则放行,并且把当前nonce值保存到容器中,这样,如果下次再使用同样的nonce来请求则容器中一定存在,那么就可以判定是无效请求了。
这里可以设置缓存的失效时间为5分钟,因为前面有效期已经做了5分钟的控制。
static Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
private static void serverVerify(String requestParam) throws Exception {
APIRequestEntity apiRequestEntity = JSONObject.parseObject(requestParam, APIRequestEntity.class);
Header header = apiRequestEntity.getHeader();
UserEntity userEntity = JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()), UserEntity.class);
// 首先,拿到参数后同样进行签名
String sign = getSHA256Str(JSONObject.toJSONString(userEntity));
if (!sign.equals(header.getSign())) {
throw new Exception("数据签名错误!");
}
// 从header中获取相关信息,其中appSecret需要自己根据传过来的appId来获取
String appId = header.getAppId();
String appSecret = getAppSecret(appId);
String nonce = header.getNonce();
String timestamp = header.getTimestamp();
// 请求时间有效期校验
long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if ((now - Long.parseLong(timestamp)) / 1000 / 60 >= 5) {
throw new Exception("请求过期!");
}
// nonce有效性判断
String str = cache.getIfPresent(appId + "_" + nonce);
if (Objects.nonNull(str)) {
throw new Exception("请求失效!");
}
cache.put(appId + "_" + nonce, "1");
// 按照同样的方式生成appSign,然后使用公钥进行验签
Map<String, String> data = Maps.newHashMap();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("sign", sign);
data.put("timestamp", timestamp);
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[0]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if (!rsaVerifySignature(sb.toString(), appKeyPair.get(appId).get("publicKey"), header.getAppSign())) {
throw new Exception("验签错误!");
}
System.out.println();
System.out.println("【提供方】验证通过!");
}
白名单机制
使用白名单机制可以进一步加强接口的安全性,一旦服务与服务交互可以使用,接口提供方可以限制只有白名单内的IP才能访问,这样接口请求方只要把其出口IP提供出来即可。
黑名单机制
与之对应的黑名单机制,则是应用在服务端与客户端的交互,由于客户端IP都是不固定的,所以无法使用白名单机制,不过我们依然可以使用黑名单拦截一些已经被识别为非法请求的IP。
限流、熔断、降级
最后限流、熔断、降级这些应该都是接口设计的标准规范,所以开放接口在设计时就更应该要考虑清楚,尤其是限流,接口提供方在对外提供时就应该与请求方约定清楚。
其他合法性校验
最后,哪些字段必填,哪些字段非必填,字段类型、长度、格式,基本业务校验也都应该确认清楚。