⼀⽂详解编程中的随机数
⼀⽂详解编程中的随机数
随机数,相信⼤家都不陌⽣,⽹上有很多⽣成随机数的⼩⼯具。直观来看,随机数就是⼀串杂乱⽆章的数字、字母、以及符号的组合, ⽐
如pSTkKIiZMOlDxOgwpIQGdlZwrJCRiHRK。但随机数真的就随机吗?真的就⽆法预测吗?什么场景下可以⽤什么⽅式来⽣成随机数呢? 这篇⽂章将为⼤家介绍随机数的类型,在程序中如何使⽤随机数,以及随机数在密码学中使⽤场景。希望能尽量地将在开发过程中需要⽤到的随机数知识都收纳在这⾥,⽅便⼤家进⾏查阅!
随机数的类型
在知乎上看到过⼀个说法,认为这个世界没有真正意义上的随机,⽐如扔骰⼦。如果能算对扔出时的转速、⽅向,并测出空⽓中的阻⼒,桌⾯的阻尼系数,骰⼦的质量 等等因素,那么就有机会算出骰⼦落地时的点数。我猜想赌神⼤概率也是基于这种原理吧。物理科学上是
有“真正”意义的随机的,那就是量⼦⼒学的不可测原理。它是由德国著名物理学家海森堡在1927年发表的论⽂《论量⼦理论运动学与⼒学的物理内涵》中提出来的。不是特别理解其中的内容,但是从字⾯上
简单的理解,就是对于微观粒⼦,它的速度和位置不能准确测量,当对其中⼀个物理量测量得越准确时,另⼀个物理量就越模糊。
从编程⾓度看,我们的随机数⽣成器分为两种⼤类型,⼀种是真随机数⽣成器,⼀种是伪随机数⽣成器。
真随机数⽣成器 TRNG - True Random Number Generator
前⾯说了实际上基本没有真正意义随机,那程序和算法本⾝就更加不能产⽣真随机,但是我们可以想办法迂回地产⽣统计意义上的真随机。⽐如Linux内核的随机数发⽣器: Linux维持⼀个熵池,不断地收集⾮确定性的事件,⽐如时钟,⿏标的移动,键盘的敲击, IO的响应时间,磁盘的速度,wifi的强弱,内存的变化等等,然后基于⼀定的算法给出⼀个数。
伪随机数⽣成器 PRNG - Pseudo Random Number Genrator
如果需要快速⽣成⼤量的随机数,那么真随机数⽣成器可能由于收集不到那么多的随机事件⽽产⽣阻塞⾏为。在不需要那么⾼安全级别的随机数需求下,我们可以采⽤伪随机数⽣成器来⽣成随机数。伪随机数⽣成器⼀般是基于⼀个给定的初始值,也就是种⼦ - seed,⽤⼀定的算法来算出⼀个数。且算法内部维持⼀个内部状态,每次⽣成⼀个新的随机数,这个值都会跟着变化,这样就能产⽣不⼀样的随机数来。常见的伪随机数⽣成器的算法有:
线性同余法 - Linear Congruential Generator (简称LCG)
马特赛特旋转演算法 - Mersenne Twister.
Java中的Random() ⽤的就是线性同余法。线性同余⽅法是⽬前应⽤⼴泛的伪随机数⽣成算法,其基本思想是通过对前⼀个数进⾏线性运算并取模从⽽得到下⼀个数,递归公式为:
其中A,B,M是产⽣器所⽤到的常量
随机数的使⽤
真随机数
我们可以通过下⾯这个命令得到操作系统内核提供的外部熵随机数⽣成器:
λhead -c 32 /dev/random | openssl enc -base64
zLvAZ2vfFTUQ+ENPLdbG2F8B3wv86LM9X2s3DeymN28=
这个命令将会从Linux内核的熵池中读取⼀个32位的随机数,并⽤64进制展⽰出来。我们也可以选择⽤数字的形式展⽰出来:
λcat /dev/random|tr -dc '0-9'|fold -w 10|head -n 4
023*******
4599846604
7629411051
4199097655
上⾯这个命令从熵池中4个10位的随机数,并⽤0-9展⽰出来。
但⽤这个命令的时候要⼩⼼,由于熵池中的值通过记录系统的随机事件得来的,那么就有可能有⽤完的时候,那么这时这个命令就会阻塞在这⾥,直到有系统随机事件进到熵池中才会继续。这样对程序来说不是很友好,于是操作系统的随机数⽣成器⼀般都提供另外⼀个⼯具,在熵池的随机事件⽤完之后,能⽤伪随机算法产⽣⼀个随机数给你:
λcat /dev/urandom|tr -dc 'a-zA-Z0-9'|fold -w 10|head -n 4
EK0Z3g49By
csziDZeWtO
EhHu30IcM4
PyDyY47Ah5
Golang的内置随机数⽣成器rand就是基于 /dec/urandom来实现的。
开发中常见的随机数⽣成器
这⾥我们以Java语⾔为例,介绍⼀下常见的随机数⽣成器的⽤法。
Random()
⾸先⼀起来看⼀下这个最常见的Random. Random实现了基于线性同余法的伪随机数⽣成器,其构造函数接收作为种⼦的参数seed,如果不给定seed,则默认采⽤当前时间戳作为种⼦。 下⾯的函数可以⽣成指定位数的随机字母串:
public static String ALPHA ="abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String generate_alphabetic_using_Random(int length){
Random random =new Random();
StringBuffer buffer =new StringBuffer();
int bound = ALPHA.length();
for(int i=0; i< length; i++){
buffer.append(ALPHA.Int(bound)));
}
String();
}
RandomStringUtil
如果⼤家只是偶尔需要⽤到随机字符串,不要什么特别的随机字符集的话,那么可以⽤Apache Common提供的RandomStringUtil 辅助类,可以⽅便地⽣成常见的随机字符串:
public static String generate_alphabetic_using_RandomUtils(int length){
return RandomStringUtils.randomAlphabetic(length);
}
作为⼀款良⼼包,Apache Common⼀般都会有⼀系列的实现⽅法可供选择,⽐如:
Math.random()
如果要⽣成的是随机数,那么也可以使⽤Math类的random⽅法来实现。
System.out.println("\n--- using Math.random ---");
for(int i =0; i <20; i++){
System.out.println(Math.random()*100);
}
这⾥提⼀下,Math.random()的实现其实是调⽤了Double()的,使⽤它本质上是Random的⼀个包装
随机数⽣成的并发性能问题
前⾯我们提到Random随机数⽣成器⾥⾯维护着⼀个内部状态,每次随机数的⽣成这个内部状态都需要跟着改变,这样才能⽣成不同的随机数。我们跟⼀下源码就可以发现Random的seed是⼀个AtomicLong型,当计算下⼀个随机数的时候,会⽤到CompareAndSwap (CAS)操作,如果切换失败的话,那么就重新计算下⼀个随机数:
private final AtomicLong seed;
protected int next(int bits){
long oldseed, nextseed;
AtomicLong seed =this.seed;
do{
oldseed = ();
nextseed =(oldseed * multiplier + addend)& mask;
}while(!seedpareAndSet(oldseed, nextseed));
return(int)(nextseed >>>(48- bits));
}
当并发有很多个线程都在获取下⼀个种⼦的时候,那么性能就会降下来,因为有很多failed-retry的。
有坑就填坑! 既然有并发问题,那么我们就来ThreadLocal吧。Java内置了⼀个ThreadLocalRandom类,这个类继承了Random, 但是每⼀个线程都有⼀个ThreadLocal的seed,这样当并发计算next()的时候,CAS操作就不会有太多冲突了。ThreadLocalRandom的⽤法也是很简单的, 跟Random基本⼀致:
public static String ALPHA ="abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String generate_alphabetic_using_ThreadLocalRandom(int length){
Random random = ThreadLocalRandom.current();
StringBuffer buffer =new StringBuffer();
int bound = ALPHA.length();
for(int i=0; i< length; i++){
buffer.append(ALPHA.Int(bound)));
}
String();
}
Random真的随机吗?
我们来看下⾯这个⽤法:
public static void sameSeed_generate_sameResult(long seed){
java生成随机数的方法Random random1 =new Random(seed);
Random random2 =new Random(seed);
System.out.println(String.format("random1: %d, random2: %d", Int(100), Int(100)));
}
public static void main(String[] args){
System.out.println("\n--- sameSeed_generate_sameResult ---");
sameSeed_generate_sameResult(3000);
}
你种⼦相同的Random⽣成的随机数是⼀样的:
--- sameSeed_generate_sameResult ---
random1: 17, random2: 17
这也可以理解,初始值seed⼀样,算法⼀样,那么⽣成的结果也会是⼀样的。 这种情况对于⼤多数需要随机数的场景来说,也是可以接受的。但是对于安全要求⽐较⾼的场景,⽐如密码学中,这样就容易被⼈攻击猜到随机数,⽐如Java的Random(),默认的种⼦是当前时间戳, 同余算法是公开的nextseed = (oldseed * multiplier + addend) & mask;,算法中的A,B,M这三个常量也是固定的
private static final long multiplier =0x5DEECE66DL;
private static final long addend =0xBL;
private static final long mask =(1L <<48)-1;
那么就有可能通过穷举⼀段时间内的所有seed,来求得随机数。
还是那句话,有坑填坑!
Java在security这个包⾥⾯提供了⼀个SecureRandom的类,以操作系统随机数⽣成器⽣成seed,再⽤
Hash算法SHA1计算出摘要值作为最终结果。这样以操作系统随机数⽣成器⽣成seed 加上 ⼀动有蝴蝶效应的 hash算法,计算出来的随机数更加能以被猜测出来。
public static String generate_alphaString_using_SecureRandom(int length)
throws NoSuchAlgorithmException {
//        Random random =new SecureRandom(); // default is SHA1PRNG
Random random =Instance("SHA1PRNG");
StringBuffer buffer =new StringBuffer();
int bound = ALPHA.length();
for(int i=0; i< length; i++){
buffer.append(ALPHA.Int(bound)));
}
String();
}
⽤SecureRandom的注意事项
前⾯我们说到真随机数的⽣成有两个⽅式,⼀个是/dev/random, ⼀个是/dev/urandom,不同的是/dev/random是阻塞的。
⽽SHA1PRNG 会根据JRE⾥⾯的配置选择/dec/random 还是/dev/urandom,如果⾥⾯配置的是securerandom.source=file:/dev/random, 那么当操作系统的熵池⽤完之后,你的程序在求随机数的时候会被阻塞住,直到有随机事件到来。据说Tomcat⾥⾯就是使
⽤Instance('SHA1PRNG') 的,使⽤有时初始化很慢很慢。具体的解决⽅案就是, 打
开$JAVA_PATH/jre/lib/security/java.security , 确保⾥⾯的securerandom.source配置为file:/dev/random .
随机数的应⽤场景
随机数的应⽤场景有很多,尤其是在密码学应⽤中,基本上⼤部分的密码学算法实际应⽤中都⽤到了随机数。下⾯列举⼀些常见的使⽤场景:
UUID: UUID v4 ⾥⾯是由 6个固定位 + 122个随机数来组成的
密码学算法应⽤中⽤到的随机数
密钥:对称加密算法,公开密钥算法,Message Authentication Code算法都会⽤到密钥,⽽⼤部分情况下,密钥就是随机数IV: 块密码加密中CBC迭代模式会⽤随机数作为IV,这样可以使得相同的明⽂加密出来的密⽂都不同,提⾼密码猜测的难度
nonce: 块密码算法的CTR模式后⽤到nonce
salt: 基于⼝令的加密算法会⽤到,很多Hash算法也会⽤到随机数作为盐
以上内容介绍了随机数的类型,随机数的使⽤,重点是在Java⾥⾯是怎么⽤的,以及多线程开发下的性能,密码学随机数⽣成,最后介绍了随机数在密码学中的使⽤场景。