一、引言
密码是数字世界的第一道防线。一个强密码可以有效阻止暴力破解——8 位纯小写字母密码约需 2 秒破解,而 16 位混合大小写 + 数字 + 符号的密码即使以每秒 10 亿次的尝试速度也需要数万亿年。这两者在使用体验上几乎没有区别(都是复制粘贴),但安全强度相差了 20 个数量级。
为什么大多数用户不使用强密码?因为"自己想一个包含大小写数字符号的密码"这件事本身就很痛苦。密码生成器解决了这个问题——它把"构造强密码"这个认知负担从用户转移到算法,让用户只需点一个按钮就能得到一个安全的随机密码。
从技术角度看,密码生成器的核心挑战有三个:
第一,字符池的动态组合。用户可以选择是否包含大写字母、小写字母、数字、特殊符号。每次生成时,字符池是这四种选择的动态并集。如果四种都未选中,需要保护——不能从空池中随机选取。
第二,密码强度评估。强度不是一个主观判断,而是基于长度和字符多样性的客观公式。12 位纯数字 vs 12 位混合四种类型,后者安全得多。强度评估需要即时反馈——用户调整参数的同时看到强度变化。
第三,每种字符类型至少命中一次保证。如果用户选择了大写字母,密码中应该至少包含一个大写字母。纯随机从池中取字符可能恰好没取到某类字符,需要在生成逻辑中保证每种选中类型至少出现一次。
本文将用 ArkUI 从零构建一个密码生成器。功能包括:四种字符类型切换(大写 / 小写 / 数字 / 特殊符号)、六档长度选择(8-32 位)、密码强度实时评估(弱 / 中 / 强三档 + 颜色进度条)、生成历史记录(最多 5 条)、一键生成。
阅读完本文,你将能够:
- 实现动态字符池的组合与随机选取
- 使用 Fisher-Yates 洗牌算法打乱密码字符顺序
- 设计密码强度评估的三维度模型(长度 × 种类数 × 组合策略)
- 保证每种选中字符类型在密码中至少命中一次的生成策略
二、密码生成的算法设计
2.1 字符集定义
四组字符覆盖了绝大多数网站的密码要求:
constUPPER:string='ABCDEFGHIJKLMNOPQRSTUVWXYZ';// 26 个大写字母constLOWER:string='abcdefghijklmnopqrstuvwxyz';// 26 个小写字母constDIGITS:string='0123456789';// 10 个数字constSYMBOLS:string='!@#$%^&*()_+-=[]{}|;:,.<>?';// 26 个常用特殊符号总计 88 个可选字符。用户选择的每种类型会拼接到charPool中:
charPool():string{letpool='';if(this.useUpper)pool+=UPPER;if(this.useLower)pool+=LOWER;if(this.useDigits)pool+=DIGITS;if(this.useSymbols)pool+=SYMBOLS;returnpool;}如果四种类型都选中,字符池大小为 88。如果只选中小写字母,字符池大小为 26。字符池大小直接决定了密码的"搜索空间"——破解者需要尝试的候选密码总数是poolSize ^ length。
2.2 每种类型至少命中一次
纯随机从字符池中取字符可能恰好没取到某类字符。例如用户选择了大写 + 小写 + 数字 + 符号,但随机生成的 12 位密码中恰好没有符号。这会降低密码的实际安全性,而且某些网站可能要求密码"必须包含特殊字符"。
解决方案——先生成"保底字符",每类至少一个,再用随机字符填充剩余长度,最后打乱顺序:
generate():void{constpool=this.charPool();if(pool.length===0){this.password='请至少选择一种字符类型';return;}constlen=LENGTH_PRESETS[this.lengthIndex];letresult='';// Step 1: 每类至少取一个字符consttypes:string[]=[];if(this.useUpper)types.push(UPPER);if(this.useLower)types.push(LOWER);if(this.useDigits)types.push(DIGITS);if(this.useSymbols)types.push(SYMBOLS);for(leti=0;i<types.length;i++){constchars=types[i];result+=chars[Math.floor(Math.random()*chars.length)];}// Step 2: 用字符池填充剩余长度while(result.length<len){result+=pool[Math.floor(Math.random()*pool.length)];}// Step 3: 打乱顺序this.password=this.shuffleStr(result);}三个步骤的逻辑干净且高效:保底 → 填充 → 打乱。顺序很重要——先保底确保每类至少一个字符,如果先填充再保底,可能破坏随机性。
2.3 Fisher-Yates 洗牌算法
步骤 3 的打乱使用 Fisher-Yates 洗牌算法(Knuth Shuffle):
shuffleStr(s:string):string{constarr=s.split('');for(leti=arr.length-1;i>0;i--){constj=Math.floor(Math.random()*(i+1));consttmp=arr[i];arr[i]=arr[j];arr[j]=tmp;}returnarr.join('');}Fisher-Yates 的核心:从数组末尾开始,每个位置与一个随机的前置位置(包括自身)交换。这个算法保证每个排列出现的概率相等(均匀随机分布)。
时间复杂度 O(n),空间复杂度 O(n)(split('')创建新数组)。对 32 位以下的密码长度,性能完全不是问题。
为什么不用sort(() => Math.random() - 0.5)?因为Array.sort比较器不是设计用于随机化的——它要求比较函数是确定性的(相同输入永远返回相同结果),而Math.random()不是。用 sort 洗牌会导致某些排列出现概率远高于其他排列,不符合"均匀随机"的要求。
2.4 历史记录管理
生成新密码时,当前密码(如果有效)被推入历史记录:
if(this.password.length>0&&this.history.indexOf(this.password)===-1){consth:string[]=[this.password,...this.history];if(h.length>5)h.pop();this.history=h;}两项过滤:
- 去重:
this.history.indexOf(this.password) === -1确保同一个密码不会重复存入历史。如果用户连续两次生成碰巧得到相同的密码(概率极低但可能),不会重复记录。 - 上限:最多保存 5 条历史。超过 5 条时删除最早的一条。历史过长会占据屏幕空间,且用户不太可能需要 10 次以前的密码。
this.history = h触发@State响应式更新,历史记录区域立即刷新。
三、密码强度评估
3.1 三维度评估模型
强度的评估不是简单的"长就是强",而是综合考虑三个维度:
| 维度 | 影响 | 示例 |
|---|---|---|
| 密码长度 | 最关键的因素 | 8 位 vs 16 位 = 搜索空间多 88^8 倍 |
| 字符种类数 | 影响搜索空间的基数 | 1 种 = 基数 26,4 种 = 基数 88 |
| 组合方式 | 是否"每种至少一次" | 保证策略让密码对所有字符类型免疫 |
评估函数:
strengthLevel():number{constlen=LENGTH_PRESETS[this.lengthIndex];consttypes=this.activeTypes();if(types===0)return0;if(len>=16&&types>=3)return3;// 强:长 + 多样if(len>=12&&types>=2)return2;// 中:中等长度 + 至少两种return1;// 弱:短或单一}三个等级的划分逻辑:
- 强(等级 3):长度 ≥ 16 且字符种类 ≥ 3。例如 16 位混合大小写 + 数字的密码,搜索空间 = 62^16 ≈ 4.8×10^28,暴力破解在可预见的时间内完全不可行。
- 中(等级 2):长度 ≥ 12 且字符种类 ≥ 2。例如 12 位字母 + 数字,搜索空间 = 36^12 ≈ 4.7×10^18,专业破解设备需要数天到数月。
- 弱(等级 1):其余所有情况。例如 8 位纯小写字母,搜索空间 = 26^8 ≈ 2×10^11,现代 GPU 集群可在几秒内暴力破解。
3.2 强度可视化
强度用文字标签 + 三格进度条展示:
Text(this.strengthLabel())// "弱" / "中" / "强".fontColor(this.strengthColor())// 红 / 蓝 / 绿ForEach([1,2,3],(level:number)=>{Row().width(22).height(6).borderRadius(3).backgroundColor(this.strengthLevel()>=level?this.strengthColor():'#E8E8EF')})三格进度条的工作原理:
- 等级 1(弱):第一格着色(红色),第二、三格灰色
- 等级 2(中):第一、二格着色(蓝色),第三格灰色
- 等级 3(强):三格全部着色(绿色)
颜色的语义化映射:红 = 危险/需要改进,蓝 = 注意/正常,绿 = 安全/最佳。用户无需阅读"弱/中/强"文字,仅凭颜色就能判断密码质量。
注意这里没有使用黄色——红 → 蓝 → 绿的渐变中,蓝色替代了传统方案中的黄色位置。蓝色#1677FF与白色背景下配对的对比度约 4.3:1,满足 WCAG 标准。
3.3 实时评估
强度评估在每次用户操作时重新计算——切换字符类型、改变长度、生成新密码,都会触发strengthLevel()重新执行。原因是strengthLevel()是一个纯计算属性(在 build() 中作为表达式使用),ArkUI 的响应式系统会在@State变量变化时自动重新执行。
用户调整参数后看到的不是"生成后的密码有多强",而是"用这些参数生成的密码大致有多强"。这是一个预判——在生成之前就告知用户密码的质量,帮助用户做出更好的参数选择。
四、UI 设计
4.1 整体布局
PasswordPage ├── 深色标题栏(52vp):🔐 密码生成器 + 生成按钮 ├── Scroll(可滚动内容区) │ ├── 密码显示卡片(白色,大号等宽字体居中) │ ├── 强度指示器(标签 + 三格进度条) │ ├── 长度选择区(6 个预设值胶囊按钮) │ ├── 字符类型选择区(4 行切换开关) │ └── 历史记录区(最多 5 条,每条可单独删除)4.2 密码显示卡片
生成的密码以大号等宽字体居中显示,白底圆角卡片:
Text(this.password).fontSize(20).fontColor('#1a1a2e').fontWeight(FontWeight.Bold).fontFamily('monospace').width('100%').textAlign(TextAlign.Center).maxLines(3)monospace等宽字体的选择是有意为之——在等宽字体中,每个字符占据相同的宽度,用户更容易辨识密码中的相似字符(如 I / l / 1 或 O / 0)。在比例字体中,Ill1三个字符的宽度可能非常接近,难以区分。
maxLines(3)限制密码最多显示 3 行。32 位密码在标准手机屏幕上通常需要换行(约 1.5-2 行),3 行的上限既保证了完整显示,又防止了长密码占满整个屏幕。
4.3 长度选择器
六档预设长度覆盖了最常见的密码长度需求:
[ 8 ] [ 12 ] [ 16 ] [ 20 ] [ 24 ] [ 32 ]constLENGTH_PRESETS:number[]=[8,12,16,20,24,32];选取这几个数值的原因:
- 8:许多网站的最低密码长度要求
- 12:推荐的"正常安全性"长度
- 16:推荐的"高安全性"长度
- 20 / 24 / 32:最高安全性,适合银行账户、密码管理器主密码等场景
每个按钮是圆角胶囊形,选中态蓝色实心 + 白字,未选中态灰色(#F0F0F5)。用户点击后立即生成新密码。
4.4 字符类型切换行
四行切换对应四种字符类型,每行包含三个元素:
[A-Z] 大写字母 ● (蓝色实心) [a-z] 小写字母 ○ (灰色空心) [0-9] 数字 ● (蓝色实心) [!@#] 特殊符号 ○ (灰色空心)使用@Builder封装单行,避免重复代码:
@BuildertypeRow(symbol:string,name:string,value:boolean,type:string,showBorder:boolean){Row(){Text(symbol)// 左侧图标块(44×44vp,有/无蓝色背景)Text(name)// 中间名称(layoutWeight 占据剩余空间)Text(value?'●':'○')// 右侧状态圆点(蓝色实心 / 灰色空心)}.onClick(()=>{this.toggleType(type);})}整个行可点击——不需要精确点击右侧的圆点。这种宽泛的触控区域对移动端体验至关重要。
showBorder控制是否显示底部分隔线。前三行之间有分隔线,最后一行(特殊符号)没有——因为它是列表的最后一项,下方没有需要分隔的内容。
4.5 历史记录
历史记录区域是一张白色卡片,每条记录一行显示密码(等宽字体灰色文字)+ 右侧 × 删除按钮:
历史记录 ┌──────────────────────────────────┐ │ xK9#mP2vL7nQ4 × │ ├──────────────────────────────────┤ │ aB3$cD5%fG8&h × │ ├──────────────────────────────────┤ │ Wq2#Rt9$Yu1@P × │ └──────────────────────────────────┘ 清除全部历史每条记录可单独删除(× 按钮),底部有"清除全部历史"链接。历史记录使用不可变更新:
consth:string[]=[];for(leti=0;i<this.history.length;i++){if(i!==hi)h.push(this.history[i]);}this.history=h;历史记录为空时不显示此区域——用户可能不希望看到空的历史标题。
五、完整代码结构
PasswordPage ├── Row(标题栏:🔐 密码生成器 + 生成按钮) ├── Scroll(内容区) │ ├── Column(密码显示卡片,白底圆角) │ ├── Row(强度指示器:标签 + 文字 + 三格进度条) │ ├── Text + Row(长度选择:6 个胶囊按钮) │ ├── Text + Column(字符类型:4 行 @Builder typeRow) │ └── if 有历史 → Column(历史记录卡片) │ ├── ForEach → Row(密码 + 删除按钮) │ └── Text(清除全部历史) └── 方法 ├── charPool() — 动态字符池 ├── activeTypes() — 已选类型计数 ├── strengthLevel() — 强度等级(0-3) ├── shuffleStr() — Fisher-Yates 洗牌 ├── generate() — 生成密码(保底 + 填充 + 打乱) └── toggleType() — 切换字符类型六、总结
本文从零构建了一个密码生成器。与前八篇的数据管理类和工具类应用不同,密码生成器的核心是随机性生成 + 安全评估——没有持久化数据,没有复杂列表,只有字符池组合、随机选取、洗牌算法和强度评估。
核心要点回顾:
动态字符池:四组字符(大写 26 + 小写 26 + 数字 10 + 符号 26 = 88 个字符)根据用户选择动态拼接。
charPool()返回当前有效的字符集,activeTypes()提供种类计数。三种字符类型至少命中一次:生成策略分三步——先从每种选中类型中随机取一个字符(保底),再用字符池随机填充剩余长度,最后用 Fisher-Yates 洗牌打乱顺序。这保证了密码对每种字符类型的覆盖。
Fisher-Yates 洗牌:O(n) 时间复杂度,均匀随机分布。不用
sort(() => Math.random() - 0.5)的原因是其分布不均匀且不符合 sort 比较器的语义契约。三维强度评估:长度(最关键的维度)× 字符种类数 × 组合方式。等级 1(弱)= 短或单一,等级 2(中)= ≥12 位 + ≥2 种字符,等级 3(强)= ≥16 位 + ≥3 种字符。三格进度条 + 颜色标签(红/蓝/绿)提供即时视觉反馈。
历史记录去重上限:最多 5 条,
indexOf去重防止重复记录。每条可单独删除,支持一键清除全部。使用不可变更新模式管理历史数组。等宽字体的密码显示:
fontFamily('monospace')让每个字符占据相同宽度,帮助用户区分 I/l/1 和 O/0 等易混淆字符。
密码生成器是一个小而精的工具——一个字符池、一次洗牌、一次强度评估,三件事组成一个完整的密码安全助手。它不产生数据,不管理状态(除了历史记录),但它是数字安全工具箱中最基础也最实用的工具。