用正则先行断言校验密码规则(以及何时不该这么做)
如何用先行断言在一个正则表达式里检查“至少一个大写字母、一个数字、八个字符”,为什么不限顺序的写法离不开它们,常见正则里有哪些陷阱,以及为什么把每条规则分开检查往往是更好的设计。

· 阅读约 3 分钟
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/
抓住
- Secret123
- aB3defgh
- Tr0ub4dor
- 9Lives_cat
放过
- secret123
- SECRET123
- Secretpass
- Ab1
“至少 8 个字符,包含一个大写字母、一个小写字母和一个数字。”这是人们最常让正则表达式做的事之一,也是学习先行断言(lookahead)的好机会,因为这些规则并没有说每个字符在哪里。
本文一条规则一条规则地搭建正则,讲清其中的陷阱,最后说明为什么干脆不用一个大正则。每个示例都可以交互,修改正则就能看到各行的变化。
01为什么普通正则做不到
正则从左往右读。要表达“包含一个数字”,你可以写 .*\d.*,单独用没有问题。但“包含一个数字并且包含一个大写字母”,不限顺序,就得把每种顺序都列出来:
/^(.*[A-Z].*\d.*|.*\d.*[A-Z].*)$/这样根本扩展不了。你需要一种检查规则时不在文本中前进的办法。
02先行断言只检查,不前进
先行断言 (?=…) 检查从当前位置开始是否能匹配某个内容,然后回到起点。所以开头的几个先行断言可以各自扫描整个密码,每个负责一条规则。
一条规则:“后面某处有一个数字”:
\d 前面的 .* 正是“某处”得以成立的原因。没有它,(?=\d) 的意思就变成“紧接着的下一个字符是数字”。
03叠加规则
每条规则各用一个先行断言,全部锚定在开头,最后由长度检查来消耗文本:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/应当匹配3/3
- Secret123
- aB3defgh
- 9Lives_cat
不应匹配4/4
- secret123
- SECRET123
- Secretpass
- Ab1
把它当成一份条件清单来读。加一条规则就是加一个先行断言,顺序随意:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/应当匹配2/2
- Secret123!
- p@ssW0rd
不应匹配1/1
- Secret123
禁止某样东西的先行断言写作 (?!…)。“任何地方都不能有空格”:
04常见正则里的陷阱
忘了锚点。 没有 ^ 和 $,.{8,} 只需要在某处有八个字符,而每个先行断言都从匹配恰好开始的地方运行。把 ^ 放在开头,让所有先行断言都从位置 0 出发;把 $ 放在结尾,让长度检查覆盖整个字符串。
最大长度不起作用。 没有 $ 的 .{8,64} 会愉快地匹配一个 500 字符密码的前 64 个字符。加上 $ 它才会拒绝,这才是你的本意。
“符号”范围太窄。 像 [!@#$%^&*] 这样的字符类会拒绝 ?、~、-、空格以及所有非英文字符。[^A-Za-z0-9](“不是字母也不是数字”)通常才是人们真正想表达的意思。
A 到 Z 以外的字母。 [A-Z] 不会把 É 算作大写字母。加上 u 修饰符后,\p{Lu} 和 \p{Ll} 分别表示任意大写字母和小写字母。
05何时不该这么做
一个正则只给出一个答案:通过或失败。而你的注册表单需要告诉用户漏掉了哪一条规则:
const rules = [
{ test: (p) => p.length >= 12, message: "Use at least 12 characters" },
{ test: (p) => /\d/.test(p), message: "Add a number" },
{ test: (p) => /[A-Z]/.test(p), message: "Add a capital letter" },
];
const problems = rules.filter((r) => !r.test(password)).map((r) => r.message);
每项检查都很小,易读又易改,错误提示也顺手就有了。
还值得问一句:你到底需不需要字符组成规则。NIST 的密码指南(SP 800-63B)不建议使用“一个大写字母、一个数字、一个符号”这类规则,因为它们会把人逼向 Password1!。该指南更看重长度:允许长口令(至少 64 个字符,含空格),并把新密码与已知泄露密码列表进行比对。
所以先行断言这种写法值得掌握,既适用于密码,也适用于任何“必须包含以下全部内容,顺序不限”的检查。但对于真实的注册表单,最小长度加上泄露密码检查,通常比它更好。
06试一试
抓住同时包含数字和字母的字符串,顺序不限:
抓住至少包含一个字母和至少一个数字的字符串,顺序不限。
目标 · 抓住 0/6
- a1目标
- 1a目标
- abc123目标
- 123abc目标
- x9y目标
- R2D2目标
无辜词 · 误抓 0
- abc无辜
- 123无辜
- ---无辜
- a-b无辜
- 4-5无辜
- !!无辜
输入正则——棋盘会实时更新。