鸿蒙密码强度检测:守护你的数字安全
一、前言
1.1 为什么需要密码强度检测
密码是数字世界的第一道防线。据 Verizon 2024 年数据泄露调查报告,超过 80% 的数据泄露与弱密码有关。"123456""password""qwerty"这些密码至今仍在被大量使用。这不是用户"笨",而是大多数应用在注册时没有给出足够的实时反馈——用户不知道自己设置的密码有多安全,直到被黑客攻破才知道。
密码强度检测器的价值就在于此:在用户输入密码的瞬间,给出可视化的强度反馈,引导用户设置更安全的密码。
它是一个用户体验组件,而不是一个安全组件——它不加密、不存储、不传输密码,它只是帮用户"看见"自己的密码质量。
1.2 最终效果预览
+----------------------------------+
| 密码强度检测器 |
| |
| [ •••••••••••••• ] |
| |
| ▓▓▓▓░░░░░░░░░░ |
| |
| 密码强度:良好 |
| |
| ┌──────────────────────┐ |
| │ 密码规则参考: │ |
| │ 1. 长度小于6位:弱 │ |
| │ 2. 6-7位纯数字/字母 │ |
| │ 3. 8位以上数字+字母 │ |
| │ 4. 8位以上数字+字母+ │ |
| │ 符号:强 │ |
| └──────────────────────┘ |
+----------------------------------+


用户输入密码时,下方的进度条实时更新,文本提示同步变化。
二、完整代码
@Entry
@Component
struct PwdLevelCheck {
@State pwd: string = ""
@State level: number = 0
@State tip: string = "请输入密码"
checkPwd(str: string) {
this.pwd = str
let len = str.length
let hasNum = /\d/.test(str)
let hasLetter = /[a-zA-Z]/.test(str)
let hasSymbol = /[^\w\s]/.test(str)
if(len === 0){
this.level = 0
this.tip = "请输入密码"
return
}
if(len < 6){
this.level = 1
this.tip = "密码过短,强度:弱"
}else if(len >=6 && len < 8 && (hasNum || hasLetter)){
this.level = 2
this.tip = "密码强度:一般"
}else if(len >=8 && hasNum && hasLetter){
this.level = 3
this.tip = "密码强度:良好"
}else if(len >=8 && hasNum && hasLetter && hasSymbol){
this.level = 4
this.tip = "密码强度:强"
}
}
build() {
Column({ space: 30 }) {
Text("密码强度检测器")
.fontSize(26)
.fontWeight(FontWeight.Bold)
TextInput({ text: this.pwd, placeholder: "请输入密码" })
.width("90%")
.height(50)
.borderRadius(8)
.type(InputType.Password)
.onChange((val: string) => this.checkPwd(val))
Row({ space: 4 }) {
ForEach([0,1,2,3], (idx: number) => {
Rect()
.width("22%")
.height(10)
.fill(idx < this.level ? 0xFF27AE60 : 0xFFDDDDDD)
.radius(2)
})
}
.width("90%")
Text(this.tip)
.fontSize(20)
.fontColor(this.level <=1 ? 0xFFE74C3C : 0xFF27AE60)
Column({ space: 8 }) {
Text("密码规则参考:")
.fontSize(17)
.fontWeight(FontWeight.Medium)
Text("1. 长度小于6位:强度弱")
Text("2. 6-7位纯数字/字母:强度一般")
Text("3. 8位以上数字+字母:强度良好")
Text("4. 8位以上数字+字母+符号:强度强")
}
.width("90%")
.padding(15)
.backgroundColor("#fff")
.borderRadius(10)
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
.padding(20)
.backgroundColor("#f5f7fa")
}
}
约 85 行代码,实现了从输入监听、强度计算、进度条渲染到提示文案的全链路。

三、密码强度算法设计
3.1 核心逻辑
checkPwd(str: string) {
this.pwd = str
let len = str.length
let hasNum = /\d/.test(str)
let hasLetter = /[a-zA-Z]/.test(str)
let hasSymbol = /[^\w\s]/.test(str)
// ... 根据 len + 三种字符类型组合判断强度
}
算法基于两个维度:密码长度 和 字符种类多样性。
3.1.1 三个正则表达式
let hasNum = /\d/.test(str) // 是否包含数字
let hasLetter = /[a-zA-Z]/.test(str) // 是否包含字母
let hasSymbol = /[^\w\s]/.test(str) // 是否包含特殊符号
逐一拆解:
/\d/ —— 匹配任意数字 0-9。等价于 /[0-9]/。
/[a-zA-Z]/ —— 匹配任意大小写字母。注意这里没有要求同时包含大小写,只要有任意一个字母就算。
/[^\w\s]/ —— 这是最复杂的一个。\w 匹配 [a-zA-Z0-9_](字母、数字、下划线)。\s 匹配空白字符(空格、制表符等)。[^\w\s] 匹配"既不是单词字符也不是空白字符"的字符——也就是特殊符号,如 !@#$%^&*()_+-=[]{}|;':",./<>?~ 等。
注意:下划线 _ 被排除在符号之外(因为 \w 包含它),但下划线通常也被视为符号。如果希望把下划线算作符号,可以用 /[^a-zA-Z0-9\s]/ 替代。
3.2 等级判定规则
| 等级 | level | 条件 | 文案 |
|---|---|---|---|
| 0 | 0 | 空密码 | "请输入密码" |
| 1 | 1 | 长度 < 6 | "密码过短,强度:弱" |
| 2 | 2 | 6-7位且包含数字或字母 | "密码强度:一般" |
| 3 | 3 | ≥8位且包含数字和字母 | "密码强度:良好" |
| 4 | 4 | ≥8位且包含数字、字母和符号 | "密码强度:强" |
逐步细化:
if(len === 0) {
this.level = 0
this.tip = "请输入密码"
return
}
if(len < 6) {
this.level = 1
this.tip = "密码过短,强度:弱"
}
前两个条件覆盖了空密码和过短密码。这两个是"不合格"的状态,直接给出弱评价。
else if(len >=6 && len < 8 && (hasNum || hasLetter)) {
this.level = 2
}
6-7 位且至少包含数字或字母。这里 ||(或)意味着数字和字母只要有一个出现即可。这个判定相对宽松——"123456"(纯数字 6 位)也能达到 level 2。实际业务中可能希望更严格,要求同时包含数字和字母,但当前代码遵循了"越短要求越宽"的渐进式原则。
else if(len >=8 && hasNum && hasLetter) {
this.level = 3
}
8 位以上且同时包含数字和字母(&&)。这里没有要求包含符号,所以 "abcdef12" 能到 level 3。
else if(len >=8 && hasNum && hasLetter && hasSymbol) {
this.level = 4
}
8 位以上且同时包含数字、字母、符号。这是最高等级。
3.3 算法的漏洞与改进
当前算法有一个明显的"覆盖漏洞":
如果密码长度 ≥ 8 但只有一种字符类型呢?
"aaaaaaaa" // len=8, hasNum=false, hasLetter=true, hasSymbol=false
它不满足 level 3(需要 hasNum && hasLetter),也不满足 level 4(需要 hasNum && hasLetter && hasSymbol),而且它也不满足 level 1(len < 6)和 level 2(len >=6 && len < 8)。
所以 "aaaaaaaa" 会走完所有 else if 而不进入任何一个分支,level 保持为 0,tip 保持为 "请输入密码"。
这是一个隐蔽的 bug!修复方案:
// 在最后加一个兜底
if (len >= 8 && !hasNum && hasLetter && !hasSymbol) {
this.level = 2
this.tip = "密码强度:一般(建议加入数字和符号)"
} else if (len >= 8 && hasNum && !hasLetter && !hasSymbol) {
this.level = 2
this.tip = "密码强度:一般(建议加入字母和符号)"
} else {
// 当前已有的四个判定
}
更好的方式是重构判定逻辑,使用积分制替代"阶梯式判定":
checkPwd(str: string) {
this.pwd = str
const len = str.length
const hasNum = /\d/.test(str)
const hasLetter = /[a-zA-Z]/.test(str)
const hasSymbol = /[^\w\s]/.test(str)
// 积分制:每个维度贡献分数
let score = 0
// 长度积分
if (len >= 8) score += 2
else if (len >= 6) score += 1
else if (len === 0) { score = 0; this.tip = "请输入密码"; this.level = 0; return }
else { score = 1 } // len < 6 但非空
// 字符多样性积分
const types = [hasNum, hasLetter, hasSymbol].filter(Boolean).length
score += types
// 映射到 0-4 等级
const levelMap = [0, 1, 2, 2, 3, 4]
this.level = Math.min(levelMap[Math.min(score, levelMap.length - 1)], 4)
// 文案映射
const tipMap = ["请输入密码", "密码过短,强度:弱", "强度:一般", "强度:良好", "强度:强"]
this.tip = tipMap[this.level]
}

积分制的优点:
- 无遗漏 —— 所有可能的输入组合都有对应的等级
- 可调参 —— 调整积分权重即可改变判定标准
- 可扩展 —— 加入大写字母、特殊分类等新维度时,只需增加积分项
3.4 强度判定的理论依据
密码强度的判定有两个理论基础:
信息熵(Entropy)
密码的强度理论上可以用信息熵来衡量:
E = L × log₂(C)
其中 L 是密码长度,C 是字符集大小。
| 字符集 | 大小 | 示例 |
|---|---|---|
| 数字 | 10 | 0-9 |
| 小写字母 | 26 | a-z |
| 大写字母 | 26 | A-Z |
| 符号 | 32 | !@#$%^&* |
一个 8 位、含大小写字母+数字+符号的密码:
E = 8 × log₂(10 + 26 + 26 + 32)
= 8 × log₂(94)
= 8 × 6.55
= 52.4 bits
而一个 6 位纯数字密码:
E = 6 × log₂(10) = 6 × 3.32 = 19.9 bits
52.4 bits 的密码用暴力破解需要 2⁵²·⁴ ≈ 6 × 10¹⁵ 次尝试,而 19.9 bits 只需要 2¹⁹·⁹ ≈ 10⁶ 次——差距是 6 亿倍。
NIST 指南
美国国家标准与技术研究院(NIST)在 SP 800-63B 中建议:
- 密码至少 8 位
- 不强制要求字符组合(大小写+数字+符号),因为研究表明强制复杂规则反而会导致用户写出可预测的模式(如
P@ssw0rd!) - 更推荐长密码短语(passphrase),如
correct-horse-battery-staple
这意味着我们在做强度检测时,长度应该比字符多样性权重更高。一个 16 位的全小写密码,实际强度可能高于一个 8 位的大小写+数字+符号密码。
当前代码对长度的权重还不够高——16 位纯小写字母在当前算法中只到 level 2,但它的实际强度应该达到 level 3 甚至 level 4。这是一个可以改进的方向。
四、渐进式进度条设计
4.1 进度条渲染
Row({ space: 4 }) {
ForEach([0,1,2,3], (idx: number) => {
Rect()
.width("22%")
.height(10)
.fill(idx < this.level ? 0xFF27AE60 : 0xFFDDDDDD)
.radius(2)
})
}
.width("90%")

4 段进度条,每段宽 22%(4 × 22% = 88%,剩余 12% 被 space: 4 占据——4 个间距 × 3 个间隙 = 12px,在 360px 宽度下约占 3.3% ≈ 合理)。
已完成段(idx < this.level)为绿色 #27AE60,未完成段为浅灰 #DDDDDD。
Rect 的 .radius(2) 给每段两端加了 2px 圆角,让进度条看起来不"扎手"。
4.2 为什么是 4 段而不是 5 段
level 的取值范围是 0,1,2,3,4(5 个等级),但进度条只有 4 段。
这是因为 level 0(空密码)不需要可视化展示——没有输入时不需要显示进度。所以进度条只用 4 段对应 level 1-4:
| 段数 | level | 含义 |
|---|---|---|
| ░░░░ | 0 | 空密码,不显示进度 |
| ▓░░░ | 1 | 弱 |
| ▓▓░░ | 2 | 一般 |
| ▓▓▓░ | 3 | 良好 |
| ▓▓▓▓ | 4 | 强 |
这样的设计在视觉上传递了一个隐含信息:"你需要填满这 4 段才达到最强级别",给用户一个明确的进步方向感。
4.3 颜色策略
Text(this.tip)
.fontSize(20)
.fontColor(this.level <= 1 ? 0xFFE74C3C : 0xFF27AE60)
文字颜色根据等级变化:
- level ≤ 1(空或弱)→ 红色
#E74C3C—— 警告 - level ≥ 2(一般及以上)→ 绿色
#27AE60—— 认可
这种红绿切换给了用户明确的情绪信号:红色代表"还不够",绿色代表"可以了"。
有些设计会采用更多颜色梯度:
| level | 颜色 | 情绪 |
|---|---|---|
| 0 | 灰色 | 中性 |
| 1 | 红色 | 危险 |
| 2 | 橙色 | 注意 |
| 3 | 蓝色 | 良好 |
| 4 | 绿色 | 优秀 |
ArkTS 中实现多色:
const levelColors = [0xFF999999, 0xFFE74C3C, 0xFFF39C12, 0xFF3498DB, 0xFF27AE60]
Text(this.tip).fontColor(levelColors[this.level])
4.4 进度条动画
当前的进度条是即时切换的。加入动画后,用户能感受到"强度在增长"的过程:
Rect()
.width("22%")
.height(10)
.fill(idx < this.level ? 0xFF27AE60 : 0xFFDDDDDD)
.radius(2)
.animation({
duration: 300,
curve: Curve.EaseOut
})
每个 Rect 的 fill 属性变化时会以 300ms 的淡入淡出过渡。这样当 level 从 1 跳到 3 时,第二段绿色渐渐亮起,第三段也渐渐亮起,视觉上像是进度条在"生长"。
五、输入组件配置
5.1 TextInput 配置
TextInput({ text: this.pwd, placeholder: "请输入密码" })
.width("90%")
.height(50)
.borderRadius(8)
.type(InputType.Password)
.onChange((val: string) => this.checkPwd(val))
每一个属性都有其意义:
type(InputType.Password)
这是密码输入框的关键配置。InputType.Password 会将输入的字符显示为圆点(•),防止旁人窥屏。
ArkTS 支持的输入类型:
| 类型 | 用途 | 键盘 |
|---|---|---|
Normal |
普通文本 | 默认键盘 |
Number |
数字 | 数字键盘 |
PhoneNumber |
电话号码 | 拨号键盘 |
Email |
邮箱地址 | 带 @ 的键盘 |
Password |
密码 | 默认键盘 + 掩码显示 |
NumberPassword |
数字密码 | 数字键盘 + 掩码 |
NumberPassword 是金融类 App 的常用类型——支付密码通常是纯数字,用数字键盘提升输入效率。
borderRadius(8)
8px 圆角在输入框中是标准配置。比直角柔和,但不至于像搜索框那样用大圆角。
height(50)
50px 是移动端输入框的"黄金高度"——足够大方便触控,也不会占用太多屏幕空间。
5.2 onChange 与状态更新
.onChange((val: string) => this.checkPwd(val))
onChange 回调在输入内容每次变化时触发。注意:这不是"输入完毕"才触发,而是每输入一个字符都触发。这意味着用户每打一个字,强度检测都会重新运行并更新 UI。
这是一个典型的实时反馈模式:
用户输入 'a' → checkPwd('a') → level=1, tip="弱"
用户输入 'ab' → checkPwd('ab') → level=1, tip="弱"
用户输入 'abc' → checkPwd('abc') → level=1, tip="弱"
...
用户输入 'abcdef' → checkPwd('abcdef') → level=2, tip="一般"
每个字符的变化都会触发完整的逻辑判断和 UI 更新。对于密码强度检测这种轻量级计算来说,性能完全不是问题。但如果 checkPwd 中包含了网络请求或复杂计算(比如调用密码泄漏查询 API),就需要做防抖(debounce) 了:
@State debounceTimer: number = 0
.onChange((val: string) => {
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => {
this.checkPwd(val)
}, 300) // 300ms 防抖
})
但就当前场景,同步执行立即响应是最好的用户体验。
六、UI 布局深度分析
6.1 布局树
Column (space: 30, 100%×100%, centered, bg:#f5f7fa)
├── Text("密码强度检测器") // 标题
├── TextInput(密码输入) // 输入框
├── Row (space: 4, 90% width) // 进度条
│ ├── Rect (22%) // 第1段
│ ├── Rect (22%) // 第2段
│ ├── Rect (22%) // 第3段
│ └── Rect (22%) // 第4段
├── Text("密码强度:良好") // 提示文字
└── Column (90% width, white card) // 规则卡片
├── Text("密码规则参考:")
├── Text("1. ...")
├── Text("2. ...")
├── Text("3. ...")
└── Text("4. ...")
一共 5 个主要内容区块,通过 Column({ space: 30 }) 纵向排列。
6.2 布局的关键设计决策
1. 输入框在顶部
用户打开页面的第一眼焦点应该落在输入框上。这是"首要操作"——用户不需要思考"我要做什么",他们只需要开始打字。
2. 进度条紧挨输入框
进度条在输入框正下方,两者之间没有其他元素。这样用户在输入时,视线自然下移就能看到强度反馈,不需要刻意寻找。
3. 规则卡片在最底部
规则卡片是"参考信息",不是"首要信息"。放在底部意味着:
- 首次打开时用户可以看到规则,知道强度的评判标准
- 输入过程中规则卡片存在但不抢眼,不影响主要交互
- 如果用户想确认"怎么才能到强",视线自然往下扫
6.3 背景色与卡片
.backgroundColor("#f5f7fa")
浅灰蓝背景 + 白色卡片,和快递追踪组件使用了相同配色方案。这种配色在工具型页面中很常见——它提供了一种"干净、专注"的视觉氛围。
白色卡片的圆角 10px、内边距 15px、左对齐的文字——整体呈现"信息面板"的质感,告诉用户"这是一份参考资料"。
七、@State 状态管理
7.1 三个状态的职责
@State pwd: string = "" // 密码原文
@State level: number = 0 // 强度等级 (0-4)
@State tip: string = "请输入密码" // 提示文案
三个状态分别对应数据层、逻辑层、表现层:
pwd—— 存储实际输入的密码(组件内部使用,不暴露给其他组件)level—— 驱动进度条和提示文字颜色的核心状态tip—— 直接展示给用户的文案,由checkPwd根据条件赋值
7.2 状态同步流程
用户输入
↓
onChange 触发 checkPwd(val)
↓
pwd = val (更新原文)
level = n (计算等级)
tip = "密码..." (生成文案)
↓
ArkTS 检测到三个 @State 变化
↓
TextInput 显示掩码字符
Rect 颜色更新
Text 文案和颜色更新
三个 @State 在同一函数中被修改,ArkTS 会将它们批量合并为一次渲染,不会触发三次独立的 UI 更新。
7.3 状态安全:不存储密码
这是一个容易被忽略但非常重要的问题:永远不要将密码明文存储到本地持久化存储中。
当前代码中,pwd 是 @State 变量——它只在内存中存在,当页面销毁时自动释放。这是安全的。
但如果在 checkPwd 中添加了本地存储逻辑:
// ❌ 危险的代码
checkPwd(str: string) {
AppStorage.set('user_password', str) // 永远不要这么做!
// ...
}
那就是灾难性的安全漏洞——密码被明文写入持久化存储,其他应用或恶意软件可能读取。
正确的做法:密码强度检测完全在内存中完成,不在任何持久化存储中留下痕迹。
八、正则表达式的深入探讨
8.1 三个正则的拆解
let hasNum = /\d/.test(str)
let hasLetter = /[a-zA-Z]/.test(str)
let hasSymbol = /[^\w\s]/.test(str)
/\d/.test(str)
\d 匹配任意十进制数字。在 JavaScript/TypeScript/ArkTS 中,它等价于 [0-9]。
test() 方法在找到第一个匹配时返回 true,否则返回 false。时间复杂度 O(n),n 为字符串长度。
/[a-zA-Z]/.test(str)
a-z 匹配 26 个小写字母,A-Z 匹配 26 个大写字母。合起来覆盖所有英文字母。
注意这里没有包含非英文字母(如德语 ü、法语 é、中文汉字)。对于国际化的密码输入,可能需要扩展:
let hasLetter = /[a-zA-Z\u00C0-\u024F]/.test(str) // 包含拉丁字母扩展
let hasCJK = /[\u4E00-\u9FFF]/.test(str) // 包含中文汉字
但大多数系统的密码强度检测只考虑 ASCII 字母,因为非拉丁字符在不同系统中的处理可能不一致。
/[^\w\s]/.test(str)
这是最复杂的。逐层拆解:
| 符号 | 含义 | 等价于 |
|---|---|---|
\w |
单词字符 | [a-zA-Z0-9_] |
\s |
空白字符 | [ \t\n\r\f\v] |
[^\w\s] |
非单词字符且非空白字符 | 特殊符号 |
所以这个正则匹配的字符包括:!@#$%^&*()_+-=[]{}|;':",./<>?~
注意:下划线 _ 被 \w 包含,所以下划线不被视为特殊符号。
如果希望把下划线视为符号,可以改为:
let hasSymbol = /[^a-zA-Z0-9\s]/.test(str)
8.2 test() vs match() vs exec()
ArkTS 中正则表达式的三种方法:
/\d/.test(str) // 返回 boolean,最快
str.match(/\d/) // 返回匹配结果或 null
/\d/.exec(str) // 返回匹配结果或 null,可迭代
对于"是否存在"这种布尔判断,test() 是最优选择——它在找到第一个匹配时就停止搜索,不需要构建匹配结果数组。
8.3 正则的性能考虑
对于密码强度检测这种短字符串(通常 < 128 字符),正则的性能差异可以忽略。但如果要检测超长文本(比如从文件读取的密码列表),有以下优化:
- 提前退出 ——
test()本身就是提前退出的(找到第一个匹配就返回) - 预编译正则 —— 将正则表达式声明为常量,避免重复创建
// 组件外部常量
const RE_NUM = /\d/
const RE_LETTER = /[a-zA-Z]/
const RE_SYMBOL = /[^\w\s]/
@Component
struct PwdLevelCheck {
checkPwd(str: string) {
const hasNum = RE_NUM.test(str)
const hasLetter = RE_LETTER.test(str)
const hasSymbol = RE_SYMBOL.test(str)
// ...
}
}
这样正则对象在模块加载时创建一次,后续重用,不重复编译。
8.4 完整密码校验正则
除了强度检测,完整的密码校验通常还需要检查:
// 禁止常见弱密码
const COMMON_PASSWORDS = new Set(["123456", "password", "qwerty", "111111", ...])
if (COMMON_PASSWORDS.has(str.toLowerCase())) {
this.level = 1
this.tip = "此密码过于常见,请换一个"
return
}
// 禁止连续字符
if (/(.)\1{2,}/.test(str)) {
// 同一字符连续出现 3 次以上,降低强度
}
// 禁止顺序字符
if (/(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/.test(str.toLowerCase())) {
// 包含连续字母或数字序列,降低强度
}
实际业务中的密码规则通常比本文示例复杂得多。
九、扩展与进阶
9.1 实时密码强度提示条
除了文字提示,还可以加入一个强/中/弱徽章:
@State badge: string = ""
@State badgeColor: ResourceColor = Color.Transparent
// 在 checkPwd 中
const badgeConfig = [
{ text: "", color: Color.Transparent },
{ text: "弱", color: 0xFFE74C3C },
{ text: "中", color: 0xFFF39C12 },
{ text: "良", color: 0xFF3498DB },
{ text: "强", color: 0xFF27AE60 },
]
this.badge = badgeConfig[this.level].text
this.badgeColor = badgeConfig[this.level].color
UI 中显示:
Row({ space: 12 }) {
Text(this.tip).fontSize(20)
if (this.badge) {
Text(this.badge)
.fontSize(14)
.fontColor(Color.White)
.backgroundColor(this.badgeColor)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
}
}
9.2 显示/隐藏密码切换
用户可能需要在输入时查看密码原文:
@State showPwd: boolean = false
// 切换显示
toggleShowPwd() {
this.showPwd = !this.showPwd
}
// TextInput 中
TextInput({ text: this.pwd, placeholder: "请输入密码" })
.type(this.showPwd ? InputType.Normal : InputType.Password)
加上一个眼睛图标按钮:
Row() {
TextInput(...)
Image(this.showPwd ? $r("app.media.eye_open") : $r("app.media.eye_close"))
.width(24).height(24)
.onClick(() => this.toggleShowPwd())
}
.alignItems(VerticalAlign.Center)
鸿蒙的 Image 组件支持本地资源和网络图片。这里使用 $r 引用本地资源。
9.3 密码强度条分级颜色
不同强度的进度条段数颜色可以不同:
getSegmentColor(idx: number): ResourceColor {
if (idx >= this.level) return 0xFFDDDDDD // 未完成
// 已完成段的颜色根据等级变化
switch (this.level) {
case 1: return 0xFFE74C3C // 弱 → 红色
case 2: return 0xFFF39C12 // 中 → 橙色
case 3: return 0xFF3498DB // 良 → 蓝色
case 4: return 0xFF27AE60 // 强 → 绿色
default: return 0xFFDDDDDD
}
}
Rect()
.width("22%").height(10)
.fill(this.getSegmentColor(idx))
.radius(2)
这样用户不仅能看到"到了哪一级",还能通过颜色感知"当前这一级的质量"。
9.4 非常见弱密码检测
除了长度和字符类型,真正的强度检测还需要考虑:
1. 常见密码黑名单
const WEAK_PASSWORDS = new Set([
"123456", "password", "12345678", "qwerty",
"abc123", "monkey", "123456789", "letmein",
"111111", "admin", "welcome", "iloveyou",
// 可扩展至 Top 10000 弱密码
])
if (WEAK_PASSWORDS.has(str.toLowerCase())) {
this.level = 1
this.tip = "此密码过于常见,极易被破解"
}
2. 键盘序列检测
// 检测键盘横向序列
const KEYBOARD_ROWS = [
"qwertyuiop", "asdfghjkl", "zxcvbnm",
"QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"
]
for (const row of KEYBOARD_ROWS) {
if (row.includes(str.toLowerCase())) {
// 包含键盘连续序列
}
}
3. 重复模式检测
// 检测重复模式如 "abcabc"、"123123"
if (/(.{2,})\1/.test(str)) {
// 包含重复模式
}
9.5 集成到注册页面
在实际的 App 中,密码强度检测器通常是注册页面的一部分:
@Entry
@Component
struct RegisterPage {
@State username: string = ""
@State pwd: string = ""
@State confirmPwd: string = ""
@State pwdLevel: number = 0
@State pwdTip: string = "请输入密码"
checkPwd(str: string) {
// ... 与之前相同的强度检测逻辑
}
canSubmit(): boolean {
return this.pwdLevel >= 3
&& this.pwd === this.confirmPwd
&& this.username.length >= 2
}
onSubmit() {
if (!this.canSubmit()) return
// 提交注册请求
}
build() {
Column({ space: 24 }) {
Text("注册").fontSize(26).fontWeight(FontWeight.Bold)
TextInput({ text: this.username, placeholder: "用户名" })
.width("90%").height(50)
TextInput({ text: this.pwd, placeholder: "密码" })
.width("90%").height(50)
.type(InputType.Password)
.onChange((val) => this.checkPwd(val))
// 密码强度指示器
Row({ space: 4 }) {
ForEach([0,1,2,3], (idx) => {
Rect().width("22%").height(8)
.fill(idx < this.pwdLevel ? 0xFF27AE60 : 0xFFDDDDDD)
.radius(2)
})
}
.width("90%")
Text(this.pwdTip).fontSize(14)
.fontColor(this.pwdLevel <= 1 ? 0xFFE74C3C : 0xFF27AE60)
TextInput({ text: this.confirmPwd, placeholder: "确认密码" })
.width("90%").height(50)
.type(InputType.Password)
if (this.confirmPwd && this.pwd !== this.confirmPwd) {
Text("两次密码不一致").fontColor(0xFFE74C3C).fontSize(14)
}
Button("注册")
.width("90%").height(50)
.backgroundColor(this.canSubmit() ? 0xFF27AE60 : 0xFFDDDDDD)
.enabled(this.canSubmit())
.onClick(() => this.onSubmit())
}
.width("100%").height("100%")
.padding(20)
.backgroundColor("#f5f7fa")
}
}
这样强度检测器就自然地成为了注册流程中的一个模块。
十、安全最佳实践
10.1 只在客户端做强度检测的局限性
必须明确一点:客户端的密码强度检测只是一个用户体验工具,不是安全措施。
- 恶意用户可以直接绕过前端检查发送弱密码
- 客户端的 JavaScript/ArkTS 代码可以被篡改
- 真正的密码策略应该在服务端强制执行
正确做法是双重验证:
客户端检测:实时反馈,引导用户设置强密码
服务端验证:拒绝不符合策略的密码,防止绕过
10.2 不要泄露密码策略细节
密码强度的提示信息需要注意信息泄露的风险:
// ❌ 过多的信息
"密码必须包含至少一个大写字母"
→ 攻击者知道你的密码规则包含"大写字母"这条
// ✅ 适度的信息
"密码强度:一般"
→ 只告诉用户强度等级,不暴露具体规则
在产品设计中,密码提示信息的详细程度需要在用户友好和信息安全之间取得平衡。
10.3 禁用复制粘贴
在输入密码的场景中,有些应用会禁用复制粘贴功能,以"防止密码被剪贴板窃取"。但这个做法的实际价值存疑:
- 现代操作系统会在剪贴板访问时给出提示
- 禁用复制粘贴给用户带来的不便大于带来的安全提升
- 建议默认不禁用复制粘贴,因为密码管理器(如 1Password、Bitwarden)依赖此功能
10.4 不应该做什么
总结一下密码输入中的安全红线:
| 操作 | 是否安全 | 说明 |
|---|---|---|
| 用 @State 存储密码 | ✅ | 仅在内存中,页面销毁即释放 |
| 用 console.log 打印密码 | ❌ | 日志文件可能被读取 |
| 用 AppStorage 持久化密码 | ❌ | 明文存储不可接受 |
| 通过网络发送密码 | ❌ | 除非使用 HTTPS 且是注册/登录目的 |
| 禁用密码粘贴 | ⚠️ | 不推荐,阻止了密码管理器 |
| 显示密码原文(用户主动点击) | ✅ | 用户知情即可 |
十一、单元测试思路
虽然 ArkTS 的测试框架不在本文讨论范围内,但测试思路值得一提:
11.1 测试用例设计
// 等价类划分法
// 空密码
checkPwd("")
→ level=0, tip="请输入密码"
// 短于 6 位
checkPwd("a") → level=1
checkPwd("ab1") → level=1
checkPwd("abc12") → level=1
// 6-7 位,含数字或字母
checkPwd("abcdef") → level=2 (含字母)
checkPwd("123456") → level=2 (含数字)
checkPwd("abc123") → level=2 (两者都有)
// ≥8 位,含数字和字母
checkPwd("abcdef12") → level=3
checkPwd("ABCD1234") → level=3
// ≥8 位,含数字、字母、符号
checkPwd("abc123!@") → level=4
checkPwd("P@ssw0rd!") → level=4
// 边界值
checkPwd("abcde") → level=1 (5位,边界)
checkPwd("abcdef") → level=2 (6位,边界)
checkPwd("abcdefg") → level=2 (7位,边界)
checkPwd("abcdef12") → level=3 (8位,边界)
11.2 问题发现
测试上面这组用例,你会发现一个被隐藏的问题:
checkPwd("abcdef") → 6 位,纯字母:满足 len >=6 && len < 8 && (hasNum || hasLetter) → level=2 ✅
checkPwd("123456") → 6 位,纯数字:同样满足 → level=2 ✅
checkPwd("abcdefg") → 7 位,纯字母 → level=2 ✅
checkPwd("1234567") → 7 位,纯数字 → level=2 ✅
看起来没问题。
但是 checkPwd("12345678") → 8 位,纯数字:len=8,hasNum=true,hasLetter=false,hasSymbol=false。条件检查:
len === 0❌len < 6❌len >=6 && len < 8 && (hasNum || hasLetter)❌ (len=8 不满足 len<8)len >=8 && hasNum && hasLetter❌ (hasLetter=false)len >=8 && hasNum && hasLetter && hasSymbol❌ (hasLetter=false)
全都不满足,level 保持为 0,tip 保持为 "请输入密码"。
这就回到了 3.3 节讨论的漏洞——8 位纯数字密码在这种情况下"无等级"。
checkPwd("12345678") → level=0, tip="请输入密码" ❌
这显然是一个 bug。修复方案在前面已经给出了(积分制重构或添加兜底 else 分支)。
测试的价值就在于此——不需要等到用户发现,测试用例就能暴露问题。
十二、总结
12.1 核心要点回顾
- 强度算法 —— 基于长度 + 字符多样性组合判定,但要注意纯数字/纯字母 8 位以上密码的"覆盖漏洞"
- 实时反馈 —— onChange 每次变化触发检测,用户每输入一个字符都能看到强度更新
- 渐进式进度条 —— 4 段 Rect 对应 4 个强度等级,已完成/未完成分色显示
- 正则表达式 ——
\d检测数字,[a-zA-Z]检测字母,[^\w\s]检测符号 - 状态管理 —— 三个 @State 各司其职,pwd 存储原文、level 驱动 UI、tip 展示文案
- 安全原则 —— 不在客户端存储密码,强度检测只作为体验引导,真正的校验在服务端
12.2 完整代码回顾
@Entry
@Component
struct PwdLevelCheck {
@State pwd: string = ""
@State level: number = 0
@State tip: string = "请输入密码"
checkPwd(str: string) {
this.pwd = str
let len = str.length
let hasNum = /\d/.test(str)
let hasLetter = /[a-zA-Z]/.test(str)
let hasSymbol = /[^\w\s]/.test(str)
if(len === 0){
this.level = 0
this.tip = "请输入密码"
return
}
if(len < 6){
this.level = 1
this.tip = "密码过短,强度:弱"
}else if(len >=6 && len < 8 && (hasNum || hasLetter)){
this.level = 2
this.tip = "密码强度:一般"
}else if(len >=8 && hasNum && hasLetter){
this.level = 3
this.tip = "密码强度:良好"
}else if(len >=8 && hasNum && hasLetter && hasSymbol){
this.level = 4
this.tip = "密码强度:强"
}
}
build() {
Column({ space: 30 }) {
Text("密码强度检测器").fontSize(26).fontWeight(FontWeight.Bold)
TextInput({ text: this.pwd, placeholder: "请输入密码" })
.width("90%").height(50).borderRadius(8)
.type(InputType.Password)
.onChange((val: string) => this.checkPwd(val))
Row({ space: 4 }) {
ForEach([0,1,2,3], (idx: number) => {
Rect().width("22%").height(10)
.fill(idx < this.level ? 0xFF27AE60 : 0xFFDDDDDD)
.radius(2)
})
}
.width("90%")
Text(this.tip).fontSize(20)
.fontColor(this.level <= 1 ? 0xFFE74C3C : 0xFF27AE60)
Column({ space: 8 }) {
Text("密码规则参考:").fontSize(17).fontWeight(FontWeight.Medium)
Text("1. 长度小于6位:强度弱")
Text("2. 6-7位纯数字/字母:强度一般")
Text("3. 8位以上数字+字母:强度良好")
Text("4. 8位以上数字+字母+符号:强度强")
}
.width("90%").padding(15).backgroundColor("#fff").borderRadius(10)
}
.width("100%").height("100%").justifyContent(FlexAlign.Center)
.padding(20).backgroundColor("#f5f7fa")
}
}
12.3 下一步方向
- 积分制重构 —— 用积分替代阶梯式判定,消除覆盖漏洞
- 常见密码黑名单 —— 接入 Top 10000 弱密码库
- 双因素引导 —— 在密码达到 level 3 后提示开启 2FA
- 密码健康检查 —— 对已保存的密码进行批量强度检测
- 国际化 —— 支持不同语言的密码规则(如包含中文、德语变音符号等)
密码安全是一个永远在演进的话题。技术方案会变,但核心理念不变——帮助用户在不增加负担的前提下,做出更安全的选择
更多推荐




所有评论(0)