SecureRandom产生随机数采坑记录

   日期:2020-11-09     浏览:153    评论:0    
核心提示:前言:最近上线时遇到一个很诡异的问题,这个问题在测试环境和pr环境都没问题,但是在生产环境必现,排查配置、代码是否一致等原因后,最终通过cat生成的traceId和研读代码,花费了两个小时,定位到问题是由于SecureRandom产生随机数系统熵池中数量不足,阻塞了当前线程。记录一下这个踩坑经验,希望大家规避掉类似的使用~情境描述以及随机算法介绍系统中经常有获取随机数的场景,我们通常new一个Random对象,通过random.nextInt(size)来获取一个随机数。通过jdk8中对Random类的

前言:最近上线时遇到一个很诡异的问题,这个问题在测试环境和pr环境都没问题,但是在生产环境必现,排查配置、代码是否一致等原因后,最终通过cat生成的traceId和研读代码,花费了两个小时,定位到问题是由于SecureRandom产生随机数系统熵池中数量不足,阻塞了当前线程。记录一下这个踩坑经验,希望大家规避掉类似的使用~

情境描述以及随机算法介绍

系统中经常有获取随机数的场景,我们通常new一个Random对象,通过random.nextInt(size)来获取一个随机数。通过jdk8中对Random类的解释,我们知道:

  1. Random 类使用线性同余法 linear congruential formula 来生成伪随机数
  2. 两个 Random 实例,如果使用相同的种子 seed,那他们产生的随机数序列也是一样的。
  3. Random 是线程安全的,你的程序如果对性能要求比较高的话,推荐使用 ThreadLocalRandom。
  4. Random 不是密码学安全的,加密相关的推荐使用 SecureRandom。
    从下面的源码中可以看到,Random 的默认使用当前系统时钟来生成种子 seed。
    private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
    // 默认使用当前系统时钟来生成种子seed
    public Random() { 
        this(seedUniquifier() ^ System.nanoTime());
    }
	// 指定初始化种子seed
    public Random(long seed) { 
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else { 
            // 子类可能重写了setSeed方法
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }
	// 线性同余算法
    private static long seedUniquifier() { 
        for (;;) { 
            long current = seedUniquifier.get();
            long next = current * 181783497276652981L;
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }

在上面介绍Random类时,该类的解释加密相关的推荐使用 SecureRandom类,所以我们看下Random的子类SecureRandom类的解释,主要有以下几点:

  1. 该类提供了能满足加密要求的强随机数生成器。
  2. 许多SecureRandom实现都是伪随机的,数字发生器(PRNG),这意味着他们使用确定性算法,从一个真正的随机种子产生一个伪随机序列。
  3. 传递给 SecureRandom 种子必须是不可预测的,seed 使用不当引发的安全漏洞,比如: 比特币电子钱包漏洞。
    // 超类构造函数的调用将导致调用到SecureRandom类重写的setSeed方法
    public SecureRandom() { 
        super(0);
        getDefaultPRNG(false, null);
    }
    // 
    private void getDefaultPRNG(boolean setSeed, byte[] seed) { 
        String prng = getPrngAlgorithm();
        if (prng == null) { 
            // bummer, get the SUN implementation
            // SUN提供程序
            prng = "SHA1PRNG";
            this.secureRandomSpi = new sun.security.provider.SecureRandom();
            this.provider = Providers.getSunProvider();
            if (setSeed) { 
                this.secureRandomSpi.engineSetSeed(seed);
            }
        } else { 
        	// NativePRNG算法,使用/dev/random或/dev/urandom获取种子,启动应用程序时可以通过参数 -Djava.security.egd=file:/dev/urandom 来指定seed源。使用/dev/random会阻塞线程直到足够的熵可用
            try { 
                SecureRandom random = SecureRandom.getInstance(prng);
                this.secureRandomSpi = random.getSecureRandomSpi();
                this.provider = random.getProvider();
                if (setSeed) { 
                    this.secureRandomSpi.engineSetSeed(seed);
                }
            } catch (NoSuchAlgorithmException nsae) { 
                throw new RuntimeException(nsae);
            }
        }
        if (getClass() == SecureRandom.class) { 
            this.algorithm = prng;
        }
    }

由上面的分析,Random产生的随机数不够随机,并且为了提升性能和随机性,Sonar建议定义一个 Random 单例来统一产生随机数, 建议使用 SecureRandom.getInstanceStrong() 。我们根据Sonar建议改掉之后,忽略了一个问题:使用/dev/ random会阻塞线程直到足够的熵可用。
所以上线当天的现象是:同一份代码配置相同其他环境都ok,但是生产环境总是接口执行超时,场景无法重现。

问题定位

由于该接口调用了很多三方接口,通过traceId分析日志,发现该接口调用日志和部分三方日志时间为00:32分,并且接口响应rt也无异常,但是另一些三方日志竟然在01:16分才打印出来,很显然出现了线程阻塞问题,通过日志给出的线索,发现执行到 SecureRandom.getInstanceStrong() 方法后就阻塞了。

 private Random rand = SecureRandom.getInstanceStrong();
 this.rand.nextInt(hasNoRuleSalerId.size())

通过查询资料发现SecureRandom.getInstanceStrong() 方法在 linux 环境下使用 /dev/random 生成种子,其实现原理是:操作系统收集了一些随机事件,比如鼠标点击、键盘点击、磁盘活动等,SecureRandom 使用这些随机事件作为种子。当服务器缺乏”活动”时,就会等待种子,从而阻塞线程。

  1. /dev/random 设备会返回小于熵池噪声总数的随机字节。/dev/random 可生成高随机性的公钥或一次性密码本。若熵池空了,对/dev/random的读操作将会被阻塞
  2. /dev/random 的一个副本是 /dev/urandom (“unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于 /dev/random 的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

问题解决

  1. 在不要求强随机性和安全性的业务场景下,推荐使用new Random()来获取随机数。
  2. 如果需要强随机性的业务场景,只需使用去空参数构造函数new SecureRandom(),让系统选择最好的随机数生成器,但是一些要求非常高速的操作情况下,SecureRandom中的随机算法稍差些。
  3. 取其精华去其糟粕,我们也可以使用Random时,手动写工具类,在获取随机数方法中增加随机性因子来达到我们的要求。
	public synchronized long nextId() { 
        Long id = Instant.now().toEpochMilli();
        int asInt = new Random().ints(0, (999 + 1)).findFirst().getAsInt();
        return id + asInt;
    }

参考文档:https://www.cnblogs.com/xiekun/p/11938196.html

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服