在我们平时进行前后端分离项目开发和调用外部功能时,都会使用API接口形式与服务器进行数据通信,而对于网页或者app,只要通过抓包就可以清楚的知道这个请求获取到的数据,数据充满着被盗用、伪造的风险,所以如何保证API调用时数据的安全性是个非常重要的问题。
通常的解决方案有:
1、通信使用https
2、请求签名,防止参数被篡改
3、时间戳超时机制
4、防重放,防止接口被第二次请求,防采集
为了解决在使用HTTP时出现这种情况:用户注册的请求在到达服务器之前,就已经被人截获了,用户名和密码被截获导致后续服务器与客户端之间的信息交流变得完全透明化。
HTTPS(全称:Hyper Text Transfer Protocol over SecureSocket Layer),在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS在HTTP的基础上加入SSL层,这是安全基础,因此加密的详细内容就需要SSL。HTTPS解决的问题不只是加密,更重要的是认证,证明服务器是自己需要的那个服务器。
客户端和服务端在传输数据之前,会对双方进行身份认证,认证成功建立连接。数据传输的机密性,一旦安全连接建立后,在传输的过程中,会对数据进行加密,那么就算在中间拿到了https传输的数据,也都是经过加密的。
SpringBoot使用https:将证书放到resources目录下,然后修改application.yml配置文件
server:
port:2720
ssl:
key-store: classpath:spdb.key
key-store-password: 123456
key-password: 123456
在全报文加密传输过程中,可以很好体会到url签名的作用:
1)首先给客户端分配一个私钥用于URL签名加密,对通信的参数按key进行字母排序放入数组中;
2)然后对排完序的数组键值对用&连接,形成用于加密的参数字符串;
3)在加密的参数字符串前面或者后面加上私钥,然后用md5进行加密,得到签名signature,然后和请求接口一起传给服务器。
http://url/getInfo?id=1&name=xiaoming&time=20201011&sign=e10adc3949ba59abbe56e057f20f883e
服务端接收到请求后,用同样的算法获得服务器的sign,对比客户端的sign是否一致,如果一致请求有效。
客户端每次请求接口都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如:1分钟),则认为该请求失效。时间戳超时机制是防御DOS攻击的有效手段。
http://url/getInfo?id=1&timetamp=1559396263
API重放攻击(Replay Attacks)又称重播攻击、回放攻击。他的原理就是把之前窃听到的数据原封不动的重新发送给接收方。HTTPS并不能防止这种攻击,虽然传输的数据是经过加密的,窃听者无法得到数据的准确定义,但是可以从请求的接收方地址分析出这些数据的作用。比如用户登录请求时攻击者虽然无法窃听密码,但是却可以截取加密后的口令然后将其重放,从而利用这种方式进行有效的攻击。
所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,重放攻击是计算机世界黑客常用的攻击方式之一。
通常解决重放问题有很多方案:
(1)时间戳timestamp:通过定义时间timestamp来决定请求从发出到达服务器的时间,比如60s。服务器在收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。
(2)基于nonce: nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,我们这里为了方便起见,直接使用时间戳的16进制,实际使用时可以加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。
我们将每次请求的nonce参数存储到一个“集合”中,可以json格式存储到数据库或缓存中。
每次处理HTTP请求时,首先判断该请求的nonce参数是否在该“集合”中,如果存在则认为是非法请求。
nonce参数在首次请求时,已经被存储到了服务器上的“集合”中,再次发送请求会被识别并拒绝。
nonce参数作为数字签名的一部分,是无法篡改的,因为黑客不清楚token,所以不能生成新的sign。
(3)基于timestamp和nonce: 我们在timestamp方案的基础上,加上nonce参数,因为timstamp参数对于超过60s的请求,都认为非法请求,所以我们只需要存储60s的nonce参数的“集合”即可。
// 获取token
String token = request.getHeader("token");
// 获取时间戳
String timestamp = request.getHeader("timestamp");
// 获取随机字符串
String nonceStr = request.getHeader("nonceStr");
// 获取请求地址
String url = request.getHeader("url");
// 获取签名
String signature = request.getHeader("signature");
// 判断参数是否为空
if (StringUtils.isBlank(token) || StringUtils.isBlank(timestamp) || StringUtils.isBlank(nonceStr) || StringUtils.isBlank(url) || StringUtils.isBlank(signature)) {
//非法请求
return;
}
//验证token有效性,得到用户信息
UserTokenInfo userTokenInfo = TokenUtils.getUserTokenInfo(token);
if (userTokenInfo == null) {
//token认证失败(防止token伪造)
return;
}
// 判断请求的url参数是否正确
if (!request.getRequestURI().equals(url)){
//非法请求 (防止跨域攻击)
return;
}
// 判断时间是否大于60秒
if(DateUtils.getSecond()-DateUtils.toSecond(timestamp)>60){
//请求超时(防止重放攻击)
return;
}
// 判断该用户的nonceStr参数是否已经在redis中
if (RedisUtils.haveNonceStr(userTokenInfo,nonceStr)){
//请求仅一次有效(防止短时间内的重放攻击)
return;
}
// 对请求头参数进行签名
String stringB = SignUtil.signature(token, timestamp, nonceStr, url,request);
// 如果签名验证不通过
if (!signature.equals(stringB)) {
//非法请求(防止请求参数被篡改)
return;
}
// 将本次用户请求的nonceStr参数存到redis中设置60秒后自动删除
RedisUtils.saveNonceStr(userTokenInfo,nonceStr,60);
//开始处理合法的请求