在鸿蒙应用开发中,生成临时token、请求签名nonce或本地加密密钥时,很多开发者第一反应是顺手用Math.random()。但如果你所在的项目有安全评审,这个写法大概率会被打回——Math.random()基于伪随机算法(PRNG),输出序列存在统计规律,给定足够样本理论上可以推导后续值,完全不适合安全敏感场景。鸿蒙的ASCF(Application Security Configuration Framework,应用安全配置框架)提供了has.getRandomValues这个API,底层基于密码学安全伪随机数生成器(CSPRNG),生成的随机数不可预测,才是做token、nonce、密钥、验证码的正确选择。
先说结论:非安全场景(UI动画、随机背景色、列表洗牌)用Math.random()完全够用;安全相关场景必须用has.getRandomValues。下面围绕这个API的用法、边界条件和实际项目中的适配细节展开。
一、API基本用法与返回值类型
has.getRandomValues的接口签名很简洁,只有一个必填参数length,表示要生成随机数的字节数,最大支持1048576字节(1MB)。成功回调里返回的randomValues是ArrayBuffer类型,而不是直接可用的字符串。
- has.getRandomValues({ length: 16, // 生成16字节随机数 success: function(res) { // res.randomValues是ArrayBuffer console.info('随机数长度:', res.randomValues.byteLength); // 16 }, fail: function(err) { console.error('生成失败:', err); } });
复制代码
这里有个容易踩的坑:ArrayBuffer不能直接toString或JSON.stringify。笔者一开始图省事直接调用res.randomValues.toString(),得到的结果完全不是预期内容;而如果直接把ArrayBuffer放进JSON.stringify,序列化出来的是一个空对象{}。往Storage里存随机数时尤其要注意——存进去是{},取出来自然也是空的,排查半天还以为是存储模块坏了。正确做法是先用工具方法转换成字符串再存储。
二、ArrayBuffer的两种常用转换方式
ASCF提供了现成的has.arrayBufferToBase64()方法,用于将ArrayBuffer转为Base64字符串。Base64格式紧凑,16字节随机数转出来约24个字符,适合放在URL参数或HTTP Header中。
- has.getRandomValues({ length: 16, success: function(res) { let base64 = has.arrayBufferToBase64(res.randomValues); console.info('Base64:', base64); // 类似 "k7Bx3mP9qR2vN5wY" 的字符串 } });
复制代码
如果需要十六进制表示(比如做数据库主键或日志追踪ID,Hex更直观),ASCF没有直接提供转换工具,需要自己遍历Uint8Array实现。注意每个字节转Hex后不足两位要补零:
- function arrayBufferToHex(buffer) { let uint8Array = new Uint8Array(buffer); let hex = ''; for (let i = 0; i < uint8Array.length; i++) { let byte = uint8Array[i].toString(16); if (byte.length < 2) { byte = '0' + byte; } hex += byte; } return hex; }
复制代码
三、length参数的单位、上限与边界行为
length单位是字节不是位。需要128位随机数时,应该传length: 16而不是128。这个单位问题容易搞混,写代码时建议多看一眼注释。
边界行为笔者实测如下:length传0不会报错,返回一个空的ArrayBuffer;传负数会走fail回调;传超过1048576(1MB)的值也会走fail回调,但fail回调里没有返回具体错误码,报的是参数错误,排查时只能靠猜。实际开发中建议在调用前自行做参数校验,避免依赖fail回调的错误信息定位问题。
四、实战:验证码、批量生成与错误处理
生成6位数字验证码时,4字节(32位)随机数已经足够覆盖1000000以内的数字空间。核心思路是用Uint32Array读取随机值,对1000000取模后补零到6位:
- function generateVerifyCode() { return new Promise(function(resolve, reject) { has.getRandomValues({ length: 4, success: function(res) { let view = new Uint32Array(res.randomValues); let code = (view[0] % 1000000).toString(); while (code.length < 6) { code = '0' + code; } resolve(code); }, fail: reject }); }); }
复制代码
批量生成多个token有两种思路。思路一是单次调用拿到大块随机数后按偏移量切割,只发起一次API调用,效率更高,但要注意总字节数不能超过1MB上限:
- function batchGenerateTokens(count, tokenLength) { let totalBytes = count * tokenLength; if (totalBytes > 1048576) { console.error('总字节数超出上限'); return; } has.getRandomValues({ length: totalBytes, success: function(res) { let uint8 = new Uint8Array(res.randomValues); let tokens = []; for (let i = 0; i < count; i++) { let start = i * tokenLength; let slice = uint8.slice(start, start + tokenLength); tokens.push(has.arrayBufferToBase64(slice.buffer)); } console.info('生成了', tokens.length, '个token'); } }); }
复制代码
思路二是循环调用getRandomValues逐个生成,代码更直观但调用次数多。实测随机数生成本身很快,两种方案体感差异不大,可根据场景选择。
在实际项目里,has.getRandomValues主要用在三个地方:一是生成请求签名nonce,配合timestamp防止重放攻击;二是生成本地加密存储的key,例如32字节(256位)密钥,转成Base64后存入存储模块;三是生成唯一ID,12字节(96位)随机数已足够降低碰撞概率,转Hex字符串作为日志追踪ID或数据库主键。
五、容易忽视的几个安全细节
不要用Date.now()当随机数种子。时间戳可预测,攻击者知道大致时间范围就能缩小搜索空间。getRandomValues内部已经处理好种子生成问题,直接使用即可。
不要觉得"随机数就是随机数"就随意混用。Math.random()和has.getRandomValues的底层实现机制完全不同,前者是PRNG,后者是CSPRNG。是否使用加密级随机数,取决于数据是否会被攻击者利用来预测后续值。token、nonce、密钥、验证码这类直接影响安全的场景,没有偷懒的余地。
总的来说,has.getRandomValues的API本身不复杂,一个参数一个回调,真正的价值在于"用对场景"。如果后续需要生成UUID,也可以用getRandomValues生成16字节随机数后按RFC 4122格式设置版本位和变体位,不过这属于更进阶的用法了。文中涉及的has.arrayBufferToBase64()工具方法比手写Uint8Array到Base64的转换省事不少,值得收藏。
- // 常见组合速查:16字节随机数转Base64做nonce
- function generateNonce() { return new Promise(function(resolve, reject) { has.getRandomValues({ length: 16, success: function(res) { resolve(has.arrayBufferToBase64(res.randomValues)); }, fail: reject }); }); }
复制代码 |