跳到主内容
d.devtul.fun
EN
正则 · 密码

密码正则

一份用先行断言拼出来的密码强度正则,外加解释:为什么强制字符组合如今已经被当成坏建议。

正则模式

^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$

要求至少一个大写、一个小写、一个数字、一个所列符号,长度 8 到 64。每条规则都是一个零宽先行断言,不消耗字符。

在线测试这个正则

在下面直接改文本 —— 匹配在本机完成,不会上传任何内容。

其它写法

只卡长度,不卡字符类
^.{12,64}$

对齐 NIST:只设一个合理的最短长度,别再逼用户凑字符类。更好记,够长反而更难爆破。

认 Unicode 字母
^(?=.*\p{Lu})(?=.*\p{Ll})(?=.*\d).{8,64}$

用 Unicode 属性类,让带声调和非拉丁字母也算字母。需要支持 Unicode 的引擎(PCRE、.NET、较新 Java)。

能直接抄走的密码正则

这是用先行断言拼出来的经典"强密码"模式:

^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$

它一次性要求四件事——一个大写、一个小写、一个数字、一个符号——再加上总长度在 8 到 64 之间。诀窍在于,这四条要求没有一条是普通的"消费字符",它们全都是先探头看一眼、再让真正的匹配在最后发生。

为什么先行断言是对的工具

正则里普通的 token 会消费字符、把光标往前推。如果你在开头写 [A-Z],那就变成"第一个字符必须是大写",这比"某处至少有一个大写"严格得多。先行断言解决了这个问题。

  • (?=...) 是零宽正向先行断言。它断言:从当前位置起,后面能匹配这个子模式;但它不会移动光标,也不把内容算进匹配结果。
  • 断言里的 .*[A-Z] 意思是"从这儿往前任何地方有一个大写字母"。.* 负责扫描,[A-Z] 才是我们要找的东西。
  • 因为每个先行断言都回到同一个起点,你可以把四个叠在一起,它们各自独立地扫整串。

只有四个断言全过之后,.{8,64}$ 才真正消费整串密码并确认长度。检查时光标压根没动,这就是关键。

为什么 NIST 劝你别这么干

这是本页核心论点。NIST 的特别出版物 800-63B——大多数银行和政府的参考标准——明确反对强制字符组合。理由如下:

  • 强制混搭催生出 Password1! 这种"合规但最该被先试"的密码。它增加用户烦恼,却没增加真正的熵。
  • 它压制创造力:一长串随机单词组成的助记密码(类似 correct-horse-battery-staple)会因为缺个数字被拒,可它比 P@ssw0rd 强得多。
  • NIST 转而建议:给一个宽松的最短长度(8 是底线,12 以上更好)、对比已知泄露密码库、拦掉明显规律。

上面那条正则作教学例子挺好。当生产策略用,它打的是一场错误的仗。

性能与人们真正会踩的坑

先行断言模式通常很快,因为整串只被扫常数次。出问题的方式更隐蔽:

  • 忘了开头锚点:写 (?=.*[A-Z]) 却不带 ^,断言可能从字符串中间开始,放行本不该过的密码。
  • 丢了 $:如果 .{8,64} 后没有 $,引擎可能在匹配前 64 位后就不再管更长的尾巴,从而接收 200 位的密码。
  • 写成 OR 而非 AND:若误用 | 连接规则而不是叠先行断言,密码只需满足任一分支,策略被架空。
  • 换行的意外:.* 默认不跨换行,除非开 s(dotall)标志。粘贴进来的密码若夹了换行,可能悄悄绕过某条规则。

常见错误写法与正确写法

错误写法问题修正
^(?=.*[A-Z])$只检查了一条规则,其余随便在 .{n,m}$ 前把所有先行断言叠齐
^(?=.*[A-Z])(?=.*[a-z]).{8,}$缺少数字和符号要求补上 (?=.*\d) 和 (?=.*[!@#$%^&*])
^[A-Za-z\d!@#]{8,}$只在第 1 位强制了字符类用先行断言让字符类出现在任意位置
^.{8,64}$完全没有组合要求(有时反而对)若遵循 NIST 只卡长度,可以接受

严格版与宽松版

你可以收紧符号集,也可以放宽。更严的规则可能要求两个符号;更松的则用 \W 放行任意非字母数字。但要注意 \W 把空白也算进去了,于是末尾一个空格就算"符号"——通常这不是你要的。只卡长度的 ^.{12,64}$ 反而是最站得住脚的现代选择,用户也最容易满足。

该用什么替代

把真正的安全工作从正则里挪出来:

  • 先卡长度:设一个最短值(12 是不错的门槛),再设一个上限,防止超大输入打瘫服务。
  • 比对泄露库:如果密码出现在泄露数据集里就拒绝,比如用 Have I Been Pwned 的 range 接口。
  • 估计熵:做强度条时,算香农熵,或用工信部风格的 zxcvbn 估算器,它能识别字典词和键盘规律。

正则仍可以用来渲染界面提示,但真正的关卡应该是"长度 + 泄露比对",而不是字符类的障碍赛。

代码:查泄露库

import hashlib, requests

def is_breached(pw):
    h = hashlib.sha1(pw.encode()).hexdigest().upper()
    prefix, tail = h[:5], h[5:]
    resp = requests.get("https://api.pwnedpasswords.com/range/" + prefix)
    return tail in resp.text

把它和一个简单长度检查搭配,你就得到一套比四段先行断言怪兽既安全又少惹人烦的策略。

常见问题

好的密码正则长什么样?

教学用 ^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$。生产环境 NIST 更推荐只卡长度加泄露比对。

为什么 NIST 反对字符组合规则?

它们催生 P@ssw0rd 这种弱却合规的密码,又挡掉强助记密码,徒增摩擦却不增真实强度。

为什么用先行断言而不是普通 token?

先行断言检查"某处包含某类字符"却不消费字符,于是规则可以出现在串中任意位置,而非仅开头。

只用正则能管住密码强度吗?

不能。把最短长度和泄露库比对结合起来;熵和是否泄露远比字符类重要。

其它正则速查

打开完整的正则测试器