一、API重放攻击
我们在设计接口的时候,最怕一个接口被用户截取用于重放攻击。重放攻击是什么呢?就是把你的请求原封不动地再发送一次,两次...n次,重放攻击是二次请求,黑客通过抓包获取到了请求的HTTP报文,然后黑客自己编写了一个类似的HTTP请求,发送给服务器。也就是说服务器处理了两个请求,先处理了正常的HTTP请求,然后又处理了黑客发送的篡改过的HTTP请求。
如果这个正常逻辑是插入数据库操作,那么一旦插入数据库的语句写的不好,就有可能出现多条重复的数据。一旦是比较慢的查询操作,就可能导致数据库堵住等情况。
1.1 重放攻击的概念:
重放攻击是计算机世界黑客常用的攻击方式之一,所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。
二、重放攻击的防御方案
2.1 基于timestamp方案
每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间相比较,是否超过了60s,如果超过了则认为是非法的请求。
假如黑客通过抓包得到了我们的请求url:
http://www.jianshu.com?uid=3535353535353535&time=1543991604448&sign=eaba21f90e635c22d2d775731ec03a92
其中
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543991604448
String sign = MD5Utils.MD5Encode("uid=" + uid + "&time=" + time + token,"utf8");
public class MD5Utils {
private static final String hexDigIts[] = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
/**
* MD5加密
* @param origin 字符
* @param charsetname 编码
* @return
*/
public static String MD5Encode(String origin, String charsetname){
String resultString = null;
try{
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if(null == charsetname || "".equals(charsetname)){
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
}else{
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
}
}catch (Exception e){
}
return resultString;
}
public static String byteArrayToHexString(byte b[]){
StringBuffer resultSb = new StringBuffer();
for(int i = 0; i < b.length; i++){
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
public static String byteToHexString(byte b){
int n = b;
if(n < 0){
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigIts[d1] + hexDigIts[d2];
}
}
一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的time参数已经失效了。
如果黑客修改time参数为当前的时间戳,则sign参数对应的数字签名就会失效,因为黑客不知道token值,没有办法生成新的数字签名。
但这种方式的漏洞也是显而易见的,如果在60s之内进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。
2.2 基于nonce方案
nonce是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,我们这里为了方便起见,直接使用时间戳作为种子,随机生成16位的字符串,作为nonce参数。
我们将每次请求的nonce参数存储到一个redis中。 每次处理HTTP请求时,首先判断该请求的nonce参数是否在redis中,如果存在则认为是非法请求。
假如黑客通过抓包得到了我们的请求url:
http://www.jianshu.com?uid=3535353535353535&nonce=RLLUammMSInlrNWb&sign=d2f7406dfdeea3561f753d9e0d1dc320
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543993280840
String nonce = RandomUtils.getRandomChar(time);
String sign = MD5Utils.MD5Encode("uid=" + uid + "&nonce=" + nonce + token,"utf8");
public class RandomUtils {
public static String getRandomChar(long time){
Random random = new Random(time);
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 16; i++){
char c = (char)(random.nextLong() % 26 + 97);
sb.append(c);
}
return sb.toString();
}
}
nonce参数在首次请求时,已经被存储到了服务器上的redis中,再次发送请求会被识别并拒绝。
nonce参数作为数字签名的一部分,是无法篡改的,因为黑客不清楚token,所以不能生成新的sign。
这种方式也有很大的问题,那就是存储nonce的redis会越来越大,验证nonce是否存在redis中的耗时会越来越长。我们不能让nonce集合无限大,所以需要定期清理该“集合”,但是一旦该集合被清理,我们就无法验证被清理了的nonce参数了。也就是说,假设该集合平均1天清理一次的话,我们抓取到的该url,虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重放攻击的。而且存储24小时内,所有请求的“nonce”参数,也是一笔不小的开销。
2.2 基于timestamp+nonce方案
我们常用的防止重放的机制是使用timestamp和nonce来做的重放机制。
每个请求带的时间戳不能和当前时间超过一定规定的时间(60s)。这样请求即使被截取了,你也只能在60s内进行重放攻击,过期失效。
但是攻击者还有60s的时间攻击。所以我们就需要加上一个nonce随机数,防止60s内出现重复请求。
timstamp参数对于超过60s的请求,都认为非法请求;
redis存储60s内的nonce参数的集合,60s内重复则认为是非法请求。
long uid = 3535353535353535L;
String token = "fewgjiwghwoi3ji4oiwjo34ir4erojwk";
long time = new Date().getTime();//1543993979284
String nonce = RandomUtils.getRandomChar(time);
String sign = MD5Utils.MD5Encode("uid=" + uid + "&time" + time +"&nonce=" + nonce + token,"utf8");
三、服务端实现流程
服务端第一次在接收到这个nonce的时候做下面行为:
1 去redis中查找是否有key为nonce:{nonce}的string
2 如果没有,则创建这个key,把这个key失效的时间和验证time失效的时间一致,比如是60s。
3 如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。
3.1 示例
那么比如,下面这个请求:
time,nonce,sign都是为了签名和防重放使用。
time是发送接口的时间,nonce是随机串,sign是对uid,time,nonce。签名的方法可以是md5({秘要}key1=val1&key2=val2&key3=val3...)
服务端接到这个请求:
1 先验证sign签名是否合理,证明请求参数没有被中途篡改
2 再验证time是否过期,证明请求是在最近60s被发出的
3 最后验证nonce是否已经有了,证明这个请求不是60s内的重放请求