防止重放机制

作者: 爱情小傻蛋 | 来源:发表于2018-12-05 15:21 被阅读23次

    一、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内重复则认为是非法请求。
    

    http://www.jianshu.com?uid=3535353535353535&time=1543993979284&nonce=VUmVZgKxkpk_rabQ&sign=da5dba49e4211df48bb5b619358c0db0

    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 示例

    那么比如,下面这个请求:

    http://www.jianshu.com?uid=3535353535353535&time=1543993979284&nonce=VUmVZgKxkpk_rabQ&sign=da5dba49e4211df48bb5b619358c0db0

    time,nonce,sign都是为了签名和防重放使用。

    time是发送接口的时间,nonce是随机串,sign是对uid,time,nonce。签名的方法可以是md5({秘要}key1=val1&key2=val2&key3=val3...)

    服务端接到这个请求:
    1 先验证sign签名是否合理,证明请求参数没有被中途篡改
    2 再验证time是否过期,证明请求是在最近60s被发出的
    3 最后验证nonce是否已经有了,证明这个请求不是60s内的重放请求

    相关文章

      网友评论

        本文标题:防止重放机制

        本文链接:https://www.haomeiwen.com/subject/peoicqtx.html