java随机数之Random和SecureRandom,以及ThreadLocalRandom
一、前言
在一次项目的安全测试源代码扫描中,报由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)
附:ThreadLocalRandom的正确用法
1. 详细可参考:https://blog.csdn.net/sinat_27143551/article/details/106981621
2. 简单介绍
java.util.concurrent.ThreadLocalRandom
1.7增加该类,企图将它和Random结合以克服所有的性能问题,该类继承自Random。
Random用到了compareAndSet + synchronized来解决线程安全问题,虽然可以使用ThreadLocal<Random>来避免竞争,但是无法避免synchronized/compareAndSet带来的开销。考虑到性能还是建议替换使用ThreadLocalRandom(有3倍以上提升),这不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。
3. 简单案列
// 生成 [0,10)的伪随机数, 左开右闭 ThreadLocalRandom.current().nextInt(10)
参考来源:
作者:专职跑龙套
链接:https://www.jianshu.com/p/2f6acd169202
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。