那一行 JavaScript,可能把安全防線拱手讓人
有段時(shí)間我在折騰一個(gè)周末小項(xiàng)目:給登錄系統(tǒng)生成“安全令牌”。我順手就用了 Math.random()——夠快、夠簡單??墒?,這東西真能扛住涉及密碼、令牌、ID 之類的敏感場景嗎?
結(jié)論先說:不能。也是從那時(shí)候起,我認(rèn)真補(bǔ)上了“密碼學(xué)安全隨機(jī)”這一課,體驗(yàn)可以說是徹底換了血。
下面把為啥 Math.random() 不靠譜、以及如何用真·安全隨機(jī)完成同樣需求一股腦講給你,并給出可直接上手的代碼片段。
Math.random():快,但不安全
JavaScript 自帶的 Math.random() 用起來毫無阻礙,生成的是偽隨機(jī)數(shù)——做個(gè)抽獎(jiǎng)動(dòng)效、隨機(jī)顏色、小游戲都 OK;但凡扯到安全,它就不及格了。
// ? 非密碼學(xué)安全
const notSecure = Math.random();
console.log(notSecure); // 0.8394720958480349為什么?因?yàn)榭深A(yù)測。
這類 PRNG(偽隨機(jī)數(shù)發(fā)生器)基于算法與狀態(tài)演進(jìn),只要攻擊者掌握足夠上下文/輸出樣本,就可能推斷后續(xù)值。在 2025 年的攻防環(huán)境里,把口令、令牌押在它身上,等于送分題。
看起來“隨機(jī)”,本質(zhì)并不“不可預(yù)測”。
真·安全隨機(jī),得具備什么?
密碼學(xué)安全隨機(jī)數(shù)(CSPRNG)要滿足:
- 不可預(yù)測:給你再多歷史輸出,也猜不到下一個(gè);
- 不可復(fù)現(xiàn):不存在“同起點(diǎn)重現(xiàn)同序列”的穩(wěn)態(tài);
- 高熵:每一比特更像擲硬幣,無明顯模式/偏差。
瀏覽器端首選:Web Crypto API
前端要用安全隨機(jī),首推內(nèi)置的 Web Crypto API。它直接調(diào)用系統(tǒng)級熵源(硬件噪聲、OS 隨機(jī)池等),生成真正不可預(yù)測的字節(jié)序列。
比如來個(gè)“安全密碼”:
function generatePassword(length = 12) {
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?';
const allChars = lowercase + uppercase + numbers + symbols;
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes); // ? 密碼學(xué)安全
return Array.from(bytes, b => allChars[b % allChars.length]).join('');
}
const password = generatePassword(16);
console.log(password); // 例如: "K9#mP2$vN8&qR5@z"思路很簡單:用 crypto.getRandomValues() 拿到安全隨機(jī)字節(jié),再把每個(gè)字節(jié)映射到你的字符集上。沒有規(guī)律、沒有復(fù)用、沒有套路。
后端就用:Node.js crypto
到服務(wù)端,直接抄起 Node.js 內(nèi)置的 crypto 模塊,API 更全、使用場景更廣(令牌、會話、ID、整數(shù)等)。
來個(gè)“安全令牌”:
const crypto = require('crypto');
function generateSecureToken(length = 32) {
return crypto.randomBytes(length).toString('hex'); // ? 密碼學(xué)安全
}
console.log(generateSecureToken()); // 例如: "a3b9f7c2e1d8..."randomBytes() 直接取自操作系統(tǒng)的隨機(jī)池,足夠安全;十六進(jìn)制編碼,落地到數(shù)據(jù)庫/HTTP 頭/URL 都很順滑。
為什么這件事非做不可?
把 Math.random() 用在密碼/令牌上,等于給攻擊者提供可預(yù)測的入口; 而 Web Crypto / Node crypto 產(chǎn)生的高熵不可預(yù)測結(jié)果,會讓你的認(rèn)證/授權(quán)/會話安全硬起來。
一句話對照表:
Math.random():快、可預(yù)測 → 僅限非敏感(如 UI 隨機(jī)色、動(dòng)畫、樣例數(shù)據(jù))- Web Crypto:瀏覽器端密碼學(xué)安全 → 客戶端密碼、OTP、ID
- **Node
crypto**:服務(wù)端密碼學(xué)安全 → 令牌、UUID、后端識別碼
幾個(gè)即插即用的小例子
1)安全整數(shù) OTP(Web Crypto)
function generateSecureOTP(min = 100000, max = 999999) {
const range = max - min + 1;
// 4 字節(jié)足夠覆蓋 32 位無符號整數(shù)空間
const buf = new Uint8Array(4);
crypto.getRandomValues(buf);
const n = new DataView(buf.buffer).getUint32(0, false); // big-endian/無關(guān)緊要
return min + (n % range);
}
console.log(generateSecureOTP()); // 例如: 472819注意:如果場景對均勻性要求極嚴(yán),可做拒絕采樣避免模偏差;一般 OTP 使用場景,這樣已足夠。
2)數(shù)據(jù)庫唯一 ID(Node.js)
const crypto = require('crypto');
function generateSecureUUID() {
return crypto.randomUUID(); // Node 16.7+ / 14.17+ 提供
}
console.log(generateSecureUUID()); // "123e4567-e89b-12d3-a456-426614174000"什么時(shí)候用哪個(gè)?
- 只做可視化/演示/非安全:
Math.random()足矣 - 瀏覽器端涉及安全:Web Crypto(密碼、OTP、客戶端 ID)
- 服務(wù)端一切安全相關(guān):Node
crypto(令牌、會話、UUID、簽名前隨機(jī))
小坑提示(總會有的)
- Web Crypto 需要安全上下文(HTTPS 或 localhost)。
crypto.randomUUID()需要較新 Node 版本(Node 14+ 已支持該 API,但推薦 LTS/更新版本)。- 需要跨端一致時(shí),別混用兩套實(shí)現(xiàn);選定一端生成,另一端只做校驗(yàn)。
結(jié)語
下次手滑寫出 Math.random() 前,先問自己一句:這是不是敏感場景?如果答案是“是”,立刻換成 Web Crypto 或 **Node crypto**。 一行對,一整條安全鏈路都跟著穩(wěn)。
你在項(xiàng)目里用過這些安全隨機(jī)接口嗎?有沒有踩過“可預(yù)測隨機(jī)”的坑?評論區(qū)聊聊你的實(shí)戰(zhàn)經(jīng)驗(yàn)。























