java随机数之Random和SecureRandom

一、前言

  在一次项目的安全测试源代码扫描中,报由random()实施的随机数生成器不能抵挡加密攻击。其中报漏洞的源代码如下:

int number = (int) ((Math.random() * 9 + 1) * Math.pow(10, 6 -1));
String numStr = String.valueOf(number);

   其中的报漏洞的解释是这样说的,在对安全性要求较高的环境中,使用能够生成可预测值的函数作为伪随机数据源,会产生Insecure Randomness(不安全随机性)错误。电脑是一种具有确定性的机器,因此不可能产生真正的随机性,伪随机数生成器(PRNG)近似于随机算法,始于一个能计算后续数值的种子。PRNG 包括两种类型: 统计学的 PRNG 和密码学的 PRNG。 统计学的 PRNG 提供很多有用的统计属性, 但其输出结果很容易预测, 因此容易复制数值流。 在安全性所依赖的生成值不可预测的情况下, 这种类型并不适用。 密码学的 PRNG 生成的输出结果较难预测, 可解决这一问题。 为保证值的加密安全性, 必须使攻击者根本无法、 或几乎不可能鉴别生成的随机值和真正的随机值。 通常情况下, 如果并未声明 PRNG 算法带有加密保护, 那么它很可能就是统计学的 PRNG, 因此不应在对安全性要求较高的环境中使用, 否则会导致严重的漏洞(如易于猜测的密码、 可预测的加密密钥、 Session Hijacking 和 DNS Spoofing) 。

示例: 下面的代码可利用统计学的 PRNG 为购买产品后仍在有效期内的收据创建一个 URL。

String GenerateReceiptURL(String baseUrl) {
  Random ranGen = new Random();
  ranGen.setSeed((new Date()).getTime());
  return (baseUrl + ranGen.nextInt(400000000) + ".html");
}

  这段代码使用 Random.nextInt() 函数为它生成的收据页面生成“唯一”的标识符。 由于 Random.nextInt() 是统计学的 PRNG, 攻击者很容易猜到其生成的字符
串。 尽管收据系统的底层设计并不完善, 但若使用不会生成可预测收据标识符的随机数生成器(如密码学的 PRNG),就会更安全些。

二、解决方案
  当不可预测性至关重要时, 如大多数对安全性要求较高的环境都采用随机性, 这时可以使用密码学的 PRNG。 不管选择了哪一种 PRNG, 都要始终使用带有充足熵的数值作为该算法的种子。 (诸如当前时间之类的数值只提供很小的熵, 因此不应该使用。 )
  Java 语言在 java.security.SecureRandom 中提供了一个加密 PRNG。 就像 java.security 中其他以算法为基础的类那样, SecureRandom 提供了与某个特定算法集合相关的包, 该包可以独立实现。 当使用 SecureRandom.getInstance() 请求一个 SecureRandom 实例时, 您可以申请实现某个特定的算法。 如果算法可行, 那么您可以将它作为 SecureRandom 的对象使用。  如果算法不可行, 或者您没有为算法明确特定的实现方法, 那么会由系统为您选择 SecureRandom的实现方法。
  Sun 在名为 SHA1PRNG 的 Java 版本中提供了一种单独实现 SecureRandom 的方式, Sun 将其描述为计算:
“SHA-1 可以计算一个真实的随机种子参数的散列值, 同时, 该种子参数带有一个 64 比特的计算器, 会在每一次操作后加 1。 在 160 比特的 SHA-1 输出中, 只能使用64 比特的输出 1。 ”
然而, 文档中有关 Sun 的 SHA1PRNG 算法实现细节的相关记录很少, 人们无法了解算法实现中使用的熵的来源, 因此也并不清楚输出中到底存在多少真实的随机数值。尽管有关 Sun 的实现方法网络上有各种各样的猜测, 但是有一点无庸置疑, 即算法具有很强的加密性, 可以在对安全性极为敏感的各种内容中安全地使用。

三、使用:

案例1:

SecureRandom random1 = SecureRandom.getInstance("SHA1PRNG"); 
SecureRandom random2 = SecureRandom.getInstance("SHA1PRNG"); 
for (int i = 0; i < 5; i++) { 
    System.out.println(random1.nextInt() + " != " + random2.nextInt()); 
}
// 结果
// 704046703 != 2117229935
// 60819811 != 107252259
// 425075610 != -295395347
// 682299589 != -1637998900
// -1147654329 != 1418666937

案例2:获取随机字符串,参考微信(wxpay-sdk,java_sdk_v3.0.9)

import java.security.SecureRandom;
import java.util.Random;
public class WXPayUtil {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}

public static void main(String[] args) {
System.out.println(WXPayUtil.generateNonceStr());
}
}
// 结果
// oIjXt5Y9g2drzfzaR4hF9haaiDjSVDCX

 四、常用随机数的一些汇总

  对于像Math.random()等实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数,在种子数的基础上进行一定的交换,从而产生需要的随机数字。在实际的项目开发过程中,经常需要产生一些随机数值,例如网站登录中的校验数字等,或者需要以一定的几率实现某种效果,游戏程序中的物品掉落,抽奖程序等。

1. Math.random() 静态方法,产生的随机数是 0 - 1 之间的一个 double,即 0 <= random <= 1.

缺点:结果可预测

举例:

for (int i = 0; i < 10; i++) {
  System.out.println(Math.random());
}
0.3598613895606426
0.2666778145365811
0.25090731064243355
0.011064998061666276
0.600686228175639
0.9084006027629496
0.12700524654847833
0.6084605849069343
0.7290804782514261
0.9923831908303121

实现原理:

When this method is first called, it creates a single new pseudorandom-number generator, exactly as if by the expression new java.util.Random()
This new pseudorandom-number generator is used thereafter for all calls to this method and is used nowhere else.

当第一次调用 Math.random() 方法时,自动创建了一个伪随机数生成器,实际上用的是 new java.util.Random()。当接下来继续调用 Math.random() 方法时,就会使用这个新的伪随机数生成器。

源码如下:

public static double random() {
    Random rnd = randomNumberGenerator;
    if (rnd == null) rnd = initRNG(); // 第一次调用,创建一个伪随机数生成器
    return rnd.nextDouble();
}
private static synchronized Random initRNG() {
    Random rnd = randomNumberGenerator;
    return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd; // 实际上用的是new java.util.Random()
}
This method is properly synchronized to allow correct use by more than one thread. However, 
if many threads need to generate pseudorandom numbers at a great rate, it may reduce contention for each thread to have its own pseudorandom-number generator.

initRNG() 方法是 synchronized 的,因此在多线程情况下,只有一个线程会负责创建伪随机数生成器(使用当前时间作为种子),其他线程则利用该伪随机数生成器产生随机数。

因此 Math.random() 方法是线程安全的。

什么情况下随机数的生成线程不安全:

  • 线程1在第一次调用 random() 时产生一个生成器 generator1,使用当前时间作为种子。
  • 线程2在第一次调用 random() 时产生一个生成器 generator2,使用当前时间作为种子。
  • 碰巧 generator1 和 generator2 使用相同的种子,导致 generator1 以后产生的随机数每次都和 generator2 以后产生的随机数相同。

什么情况下随机数的生成线程安全: Math.random() 静态方法使用

  • 线程1在第一次调用 random() 时产生一个生成器 generator1,使用当前时间作为种子。
  • 线程2在第一次调用 random() 时发现已经有一个生成器 generator1,则直接使用生成器 generator1
public class JavaRandom {
    public static void main(String args[]) {
        new MyThread().start();
        new MyThread().start();
    }
}
class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + Math.random());
        }
    }
}

结果:

Thread-1: 0.8043581595645333
Thread-0: 0.9338269554390357
Thread-1: 0.5571569413128877
Thread-0: 0.37484586843392464

 2. 对于其它的一些随机数相关的工具类可参考java.util.Random 工具类、java.util.concurrent.ThreadLocalRandom 工具类、java.Security.SecureRandom、随机字符串.(https://www.jianshu.com/p/2f6acd169202)

参考来源:

作者:专职跑龙套
链接:https://www.jianshu.com/p/2f6acd169202
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

原文地址:https://www.cnblogs.com/damoblog/p/14341018.html