目录
前言
在新一轮需求中,我方系统需要与某一系统进行数据传输,由于这些数据需要保证安全不外泄,并在后期交付时要做等保检测,因此在设计接口实现时,需要保证数据传输的安全性。
一般来说,API接口的安全性能一般要考虑以下几点:
1. 接口请求来源合法。
2. 数据传输途中不能被篡改。
A. 请求参数不能被篡改。
B. 返回结果不能被篡改。
3. 数据加密返回,保证数据安全。
4. 防重放攻击。即将拦截请求的参数,重新用于接口调用。
5. 防伪装攻击。这种情况一般是token被劫持时,攻击者伪装请求。
一. 传输加密思路
下文中,客户端皆指接口请求方,服务端皆指提供API的接口提供方。
本次接口开发中的大体思路如下:
1. 客户端生成sign签名,调用服务端接口。
2. 服务端对sign签名进行验证。
3. 服务端对验证成功的请求,返回加密数据。
4. 客户端接收加密数据,解密并使用。
二. 加密操作的具体实现过程
服务端API接口详情:
URL:IP:PORT/api/getSecretData
请求方式:POST
请求头:Content-type:application/json
这里省略了其他业务参数,示例参数如下:
timestamp |
nonce |
sign |
1. 客户端sign签名的生成规则
以上参数中,加入sign签名,保证在未获取license以及其具体算法未被破解前的数据无法篡改;加入timestamp时间戳让sign签名更具变化;加入timestamp和nonce的组合以防止重放攻击。
参数生成规则说明:
A. nonce取的是随机数再MD5的值。
B. sign的生成则是将所有参数拼接后取MD5特征码再转base64。
客户端sign签名的Java生成代码,代码中的工具类见文末:
// 生成nonce
public String generateNonce(long timestamp) {
return Md5Util.encrypt(String.valueOf(new Random(timestamp)));
}
// sign生成
public String generateSign(long timestamp, String nonce) {
String data = "license=" + license + "×tamp=" + timestamp + "&nonce=" + nonce;
String encrypt = Md5Util.encrypt(data);
return RSAUtil.str2Base64(encrypt);
}
2. 服务端验证
接收到客户端请求的数据之后,服务端需要验证请求参数是否被修改,验证sign签名是否正常。
参数校验:
A. 首先验证时间戳,确定该请求处于当前时间的60s以内。
B. 验证nonce,在redis中查看24小时内的数据中是否存在该nonce。
C. 验证sign签名,将服务端提供的license等字段拼接,根据sign生成的相同规则判断sign的正确性。
服务器验证Java代码:
// 验证时间
public boolean checkTime(long timestamp) {
long current = System.currentTimeMillis();
return current <= (timestamp + 60 * 1000);
}
// 验证nonce
public boolean checkNonce(String nonce) {
String key = "nonce";
Set<String> nonceList = redisTemplate.opsForValue().get(key);
if (CollectionUtils.isEmpty(nonceList)) {
nonceList = new HashSet<>();
}
else if (nonceList.contains(nonce)) {
return false;
}
nonceList.add(nonce);
redisTemplate.opsForValue().set(key, nonceList, 24, TimeUnit.HOURS);
return true;
}
// 验证签名
// data为参数拼接字符串
public boolean checkSign(String data, String sign) throws IOException {
String str = RSAUtil.base642Str(sign);
return Md5Util.matches(data, str);
}
3. 服务端数据加密以及数据返回
当上述的请求验证通过后,服务端从库中获取数据后,并对要返回的数据进行加密操作。
数据加密流程:
A. 生成AES密钥种子(以下称为种子),这里的种子可以通过任意方法生成。
B. 使用种子生成AES密钥,并加密数据。
C. 使用RSA公钥加密种子。
D. 将加密数据以及种子的密文封装,并转为base64编码发送给客户端。
数据加密代码实例:
// 加密数据
public static String encryptContent(String content, String seed) throws Exception {
Map<String, String> map = new HashMap<>();
String key = Md5Utils.encrypt(seed + System.currentTimeMillis());
// 内容AES加密
String contentAES = AESUtils.encrypt(content, key);
// 种子RSA加密
String encryptSeed = RSAUtils.encrypt(key);
map.put("key", encryptSeed);
map.put("value", contentAES);
System.out.println("==============map============");
System.out.println(map);
// 转为base64
return Base64Utils.str2Base64(JSONObject.toJSONString(map));
}
4. 客户端解密数据
客户端接收到数据后,将数据解密后即可。
数据解密流程:
A. base64编码转换后,获取到封装的key,value数据。
B. 用RSA的私钥解密种子。
C.将种子用来解密加密数据,并最终获取到数据。
对应解密代码实例:
// 客户端接收数据并解密
public void decryptData(String str) {
try {
String content = Base64Utils.base642Str(str);
JSONObject json = JSONObject.parseObject(content);
String key = (String) json.get("key");
String value = (String) json.get("value");
String seed = RSAUtils.decrypt(key);
String originData = AESUtils.decrypt(value, seed);
System.out.println("============originData==========");
System.out.println(originData);
} catch (Exception e) {
e.printStackTrace();
}
}
代码可以参考:git地址
参考文章:
Java AES encryption and decryption
三. 总结
本次加密传输,大体上解决了请求数据被篡改、请求数据被重放攻击、返回数据易被破解等问题。但是,由于本次的API加解密,涉及的知识点比较多,对其中的一些概念和用法还没理解透彻,因此整体上,接口的漏洞仍旧存在,功能有待完善。