谈谈crypto.getRandomValues和Math.random并开发抽奖系统

1,501 阅读7分钟

在前端开发中,随机数生成是一个非常常见的需求,比如生成随机颜色、生成随机字符串等等。但是,由于众所周知的原因,JavaScript的随机数生成器并不可靠,因此我们需要使用一些特殊的方法来生成真正的随机数。其中一个方法就是使用Web Crypto API中的crypto.getRandomValues()方法。那究竟使用crypto.getRandomValues()Math.random()这两个方法来生成随机数有什么区别呢?

抽奖系统的代码和运行效果,在本文章的第三节哈!心急可以先移步查看。

一、先谈crypto.getRandomValues()

crypto.getRandomValues()是Web Crypto API中提供的方法,用于生成真正的随机数。它使用操作系统提供的随机数生成器生成随机数,因此生成的随机数是完全不可预测的。这种方式生成的随机数在密码学应用中非常有用,因为密码学应用需要生成高质量的随机数来确保安全。

1. 使用方法

使用crypto.getRandomValues()生成随机数需要先创建一个类型化数组,然后调用该方法方法将随机数填充到这个数组中。随机数生成的范围取决于类型化数组的类型和长度。例如,使用8位无符号整数类型化数组可以生成0到255之间的随机数,而使用16位无符号整数类型化数组可以生成0到65535之间的随机数。

代码示例: (本文第三部分用该方法写了一个抽奖系统)

// 创建一个长度为1的32位无符号整数类型化数组
const randomArray = new Uint32Array(1);
// 将随机数填充到数组中
crypto.getRandomValues(randomArray);
// 获取数组中的随机数
const randomNumber = randomArray[0];

在使用crypto.getRandomValues()方法生成随机数时,不一定非要使用32位无符号整数类型化数组。根据需要,可以使用其他类型化数组来生成不同类型的随机数。

例如,如果需要生成一个0到255之间的随机整数,可以使用8位无符号整数类型化数组。代码示例:

// 创建一个长度为1的8位无符号整数类型化数组
const randomArray = new Uint8Array(1);
// 将随机数填充到数组中
crypto.getRandomValues(randomArray);
// 获取数组中的随机数
const randomNumber = randomArray[0];

值得注意的是,crypto.getRandomValues()方法只能在HTTPS协议下使用,否则会抛出异常。因为该方法使用的是操作系统提供的真随机数生成器,生成的随机数非常高质量,适用于密码学应用等需要高质量随机数的场景。然而在HTTP协议下,生成的随机数有可能会被中间人攻击者篡改,从而破坏密码学应用的安全性。因此,为了避免这种安全问题,crypto.getRandomValues()方法只能在HTTPS协议下使用。

2. 使用场景

crypto.getRandomValues()方法适用于需要高质量随机数的场景,例如密码学应用、加密密钥生成等。这是因为crypto.getRandomValues()方法生成的是真正的随机数,生成的随机数基于硬件随机性源(如硬件噪声)生成,因此非常难以预测。这使得它非常适合用于安全目的。

虽然JavaScript中有Math.random()方法可以生成随机数,但是它并不是真正的随机数生成器。它的生成方式是伪随机,也就是说,它实际上是基于某种算法生成的,而不是真正的随机数。这使得它容易被猜测和预测,因此不能用于安全目的。

相比之下,crypto.getRandomValues()生成的是真正的随机数,这些随机数基于硬件随机性源(如硬件噪声)生成,因此非常难以预测。

二、再说Math.random()

相比之下,Math.random()方法使用的是伪随机数生成器,因此生成的随机数是可预测的。这种生成方式生成的随机数在一些简单的应用场景下是足够用的,但在需要高质量随机数的场景下,它是不够用的。Math.random()适用于许多场景,例如模拟游戏、生成测试数据、快速生成不需要高度安全性的随机数等。

1. 使用方法

使用Math.random()方法生成随机数非常简单,只需要调用Math.random()方法即可。它会返回一个[0,1)之间的浮点数随机数。下面是一个使用Math.random()生成随机数的例子:

const randomNumber = Math.random();

2. 存在缺点

  1. 不是真正的随机

    虽然Math.random()函数可以生成看起来是随机的数字,但是它并不是真正的随机。它的算法是基于伪随机数生成器(PRNG)实现的,这意味着它是通过一个确定性的算法来生成数字的,而不是真正的随机过程。这就意味着,如果我们知道了这个算法,就可以预测下一个生成的数字。

  2. 不是安全的随机

    由于Math.random()函数不是真正的随机,因此它也不是安全的随机。也就是说,它生成的数字可能会被破解或预测,从而导致一些安全问题。

    例如,在密码重置过程中使用Math.random()函数生成的随机码可能会被猜测到,从而导致账户被盗。

  3. 无法控制随机数的范围

    虽然Math.random()函数可以生成0到1之间的随机数,但是我们无法控制生成的随机数的范围。这就意味着,如果我们需要生成一个特定范围内的随机数,就需要进行额外的计算和处理。

    例如,如果我们需要生成一个1到100之间的随机数,就需要使用Math.random()函数生成0到1之间的随机数,然后将其乘以100,再加上1,最后向下取整,才能得到一个1到100之间的随机数。

3. 增加Math.random()随机性

虽然Math.random()生成的随机数是可预测的,但是我们仍然可以通过一些技巧增加它的随机性。比如,我们可以使用当前时间戳来作为Math.random()的种子,这样就能得到更加随机的结果。但是,这种方式仍然不能与crypto.getRandomValues()相比,因为它生成的随机数仍然不是真正的随机数。

增加Math.random()随机性的代码:

// 使用当前时间戳作为种子
const randomNumber = Math.floor(Math.random() * new Date().getTime());

这样生成的随机数会受到当前时间的影响,因此更难以被预测。但是,重要场景仍然不建议使用。

三、用crypto.getRandomValues()开发抽奖系统

这里就不用Math.random() 来写抽奖系统了,毕竟是伪随机,实现也简单,大家自己搞得定。这里使用大家比较少用到的crypto.getRandomValues()来开发抽奖系统,确保生成的随机数是高质量的。

1. 运行效果图

可以在码上掘金上体验:https://code.juejin.cn/pen/7220253949234774077

抽奖.gif

2. 代码实现

HTML和css代码可以在码上掘金查看,这里只展示js代码下面是实现的抽奖系统的js核心代码:

const prizeList = ['奖品1', '奖品2', '奖品3', '奖品4', '奖品5', '奖品6', '奖品7', '奖品8', '奖品9', '奖品10'];
const startBtn = document.querySelector('#start-btn');
const roulette = document.querySelector('.roulette');
const prize = document.querySelector('.prize');
const prizeAngle = 360 / prizeList.length;
let isSpinning = false;
//随机函数
function getRandomNumberInRange(min, max) {
  const range = max - min + 1;
  const randomArray = new Uint32Array(1);
  let randomNumber = 0;

  do {
    crypto.getRandomValues(randomArray);
    randomNumber = randomArray[0] % range;
  } while (randomNumber >= range);

  return min + randomNumber;
}
//摆放奖品
function setPricePos() {
  let everyAngle = 360 / prizeList.length;
  console.log(everyAngle)
  let parentNode = document.getElementById('board');
  let childNode = null;
  prizeList.forEach((item, idx) => {
    childNode = document.createElement('span');
    childNode.textContent = item;
    childNode.style.cssText = `transform:rotate(${everyAngle * idx}deg);`;
    parentNode.appendChild(childNode);
  })

}
setPricePos()
//开始抽奖
function startSpin() {
  startBtn.disabled = true;
  prize.textContent = '抽奖中';
  if (isSpinning) {
    return;
  }
  isSpinning = true;

  const numberOfPrizes = prizeList.length;
  const winningIndex = getRandomNumberInRange(0, numberOfPrizes - 1);
  const winningPrize = prizeList[winningIndex];

  let endAngle = prizeAngle * winningIndex + getRandomNumberInRange(0, prizeAngle - 1);
  endAngle += 360 * 5;

  const spinDuration = 5000; // 旋转持续时间

  const animate = roulette.animate([
    { transform: 'rotate(0deg)' },
    { transform: `rotate(${endAngle}deg)` }
  ], {
    duration: spinDuration,
    easing: 'ease-in-out'
  });

  animate.addEventListener('finish', () => {
    startBtn.disabled = false;
    prize.textContent = winningPrize;
    isSpinning = false;
  });

}

startBtn.addEventListener('click', startSpin);

由于crypto.getRandomValues()生成的随机数是一个32位的无符号整数,因此在使用它生成指定范围内的随机数时,需要进行一些额外的处理,以避免生成的随机数偏离指定范围。上面的代码中,使用了一个do-while循环来确保生成的随机数在指定范围内。

四、写在最后

crypto.getRandomValues()Math.random()这两个方法的区别在于生成随机数的方式不同。crypto.getRandomValues()使用操作系统提供的真随机数生成器,保证了生成的随机数的高质量,适用于密码学应用等需要高质量随机数的场景;

大家在实际开发中,需要根据具体情况选择合适的随机数生成方法。如果需要高质量的随机数,那么应该使用crypto.getRandomValues();如果只是在一些简单的应用场景下需要生成随机数,那么可以使用Math.random()。但是,无论使用哪种方法,都需要注意随机数的安全性,避免被破解而引发安全问题。

本文正在参加「金石计划」