一、前言

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]
}

积分制的优点:

  1. 无遗漏 —— 所有可能的输入组合都有对应的等级
  2. 可调参 —— 调整积分权重即可改变判定标准
  3. 可扩展 —— 加入大写字母、特殊分类等新维度时,只需增加积分项

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 字符),正则的性能差异可以忽略。但如果要检测超长文本(比如从文件读取的密码列表),有以下优化:

  1. 提前退出 —— test() 本身就是提前退出的(找到第一个匹配就返回)
  2. 预编译正则 —— 将正则表达式声明为常量,避免重复创建
// 组件外部常量
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。条件检查:

  1. len === 0 ❌
  2. len < 6 ❌
  3. len >=6 && len < 8 && (hasNum || hasLetter) ❌ (len=8 不满足 len<8)
  4. len >=8 && hasNum && hasLetter ❌ (hasLetter=false)
  5. len >=8 && hasNum && hasLetter && hasSymbol ❌ (hasLetter=false)

全都不满足,level 保持为 0,tip 保持为 "请输入密码"。

这就回到了 3.3 节讨论的漏洞——8 位纯数字密码在这种情况下"无等级"。

checkPwd("12345678") → level=0, tip="请输入密码" ❌

这显然是一个 bug。修复方案在前面已经给出了(积分制重构或添加兜底 else 分支)。

测试的价值就在于此——不需要等到用户发现,测试用例就能暴露问题。


十二、总结

12.1 核心要点回顾

  1. 强度算法 —— 基于长度 + 字符多样性组合判定,但要注意纯数字/纯字母 8 位以上密码的"覆盖漏洞"
  2. 实时反馈 —— onChange 每次变化触发检测,用户每输入一个字符都能看到强度更新
  3. 渐进式进度条 —— 4 段 Rect 对应 4 个强度等级,已完成/未完成分色显示
  4. 正则表达式 —— \d 检测数字,[a-zA-Z] 检测字母,[^\w\s] 检测符号
  5. 状态管理 —— 三个 @State 各司其职,pwd 存储原文、level 驱动 UI、tip 展示文案
  6. 安全原则 —— 不在客户端存储密码,强度检测只作为体验引导,真正的校验在服务端

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 下一步方向

  1. 积分制重构 —— 用积分替代阶梯式判定,消除覆盖漏洞
  2. 常见密码黑名单 —— 接入 Top 10000 弱密码库
  3. 双因素引导 —— 在密码达到 level 3 后提示开启 2FA
  4. 密码健康检查 —— 对已保存的密码进行批量强度检测
  5. 国际化 —— 支持不同语言的密码规则(如包含中文、德语变音符号等)

密码安全是一个永远在演进的话题。技术方案会变,但核心理念不变——帮助用户在不增加负担的前提下,做出更安全的选择

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐