SecureRandom产生随机数采坑记录

前言:最近上线时遇到一个很诡异的问题,这个问题在测试环境和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

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值