两条与密码验证相关的正则表达式问题
- 问题1: 密码验证:由且仅由数字、字母(大小写)、特殊符号(@ % &…)组成,三者缺一不可,密码不少于8位。
- 问题2: 十位的数字、字母组合密码,其中包含4位数字和6位字母。
感兴趣的话,建议您在读下文之前,自己思考一下解法,以免被我的思路干扰。
Stage0
对于问题1,它需要满足的条件如下:
- 8位以上;
- 必须包含1位以上的数字;
- 必须包含1位以上的字母;
- 必须包含1位以上的特殊字符。
对于这样的要求,简单使用[0-9a-za-Z@%&]{8,}来匹配的。因此它也匹配像00000000、1111aaaaa这样只含一种或两种字符的字符串。因此,我们要加上更为严格的限制条件,以便匹配更精确。
Stage1
代码:
1 | [0-9a-zA-Z@%&]+\d |
字母必须出现一次,则对于每个字符位置来说,它应该是这样的:
代码:
1 | [0-9a-zA-Z@%&]+[a-zA-Z] |
特殊字符必须出现一次,则对于每个字符位置来说,它应该是这样的:
代码:
1 | [0-9a-zA-Z@%&]+[@%&] |
这三个条件必须同时满足,因此:
代码:
1 | (?=[0-9a-zA-Z@%&]+\d)(?=[0-9a-zA-Z@%&]+[a-zA-Z])(?=[0-9a-zA-Z@%&]+[@%&]).{8,} |
为了保证字符整行匹配,需要加上条件^$:
代码:
1 | ^(?=[0-9a-zA-Z@%&]+\d)(?=[0-9a-zA-Z@%&]+[a-zA-Z])(?=[0-9a-zA-Z@%&]+[@%&]).{8,}$ |
它匹配的是,8位(包括)以上字符,由且仅由数字、字母和特殊字符组成。
Stage2
上图中Test部分中彩色部分为正则表达所匹配的字串。但是前三条是符合要求的,却不被匹配。之所以会出现这样的情况,是因为在环视条件中使用了+量词,这会将本来用作辅助验证的字符被消耗掉,原本合格的字串被误认为不合格了。
问题出在+上,因此我们使用*量词,这样就好多了。正则表达式为:
1 | ^(?=[0-9a-zA-Z@%&]*\d)(?=[0-9a-zA-Z@%&]*[a-zA-Z])(?=[0-9a-zA-Z@%&]*[@%&]).{8,}$ |
匹配效果如下所示:
Stage3
但是问题依然存在。测试发现,像这样的字串也是匹配的,但是它显然不是合格的密码字串:
之所以出现这样的问题,是因为stage2代码中
1 | .{8,}$ |
前边千辛万苦使用[0-9a-zA-Z@%&]所界定的条件,在这里轻轻松松被破坏了。stage2其实只管前8位,只要前8位字符符合要求,它就认为万事大吉了。
认识到这一点,我写个一条长长的正则式:
1 | ^(?=[0-9a-zA-Z@%&]*\d)(?=[0-9a-zA-Z@%&]*[a-zA-Z])(?=[0-9a-zA-Z@%&]*[@%&])[0-9a-zA-Z@%&]{8,}$ |
但是这条正则表达太复杂了。能不能短一些呢?当然可以。从上文可以看出,前边其实不必界定太复杂的条件,只要在最后加上条件判断即可。因此,正则表达式可以改为:
1 | ^(?=.*\d)(?=.*[a-zA-Z])(?=.*[@%&])[0-9a-zA-Z@%&]{8,}$ |
这样一来,我们就得到了这道题迄今为止最简洁的解法。
同理可得,第二道题的解法是:
1 | ^(?=.*\d)(?=.*[a-zA-Z])(?=.*[@%&])[0-9a-zA-Z@%&]{8,}$ |
不多解释。
在思考本题的过程中,感谢创亿无限在stage2的测试,感谢余晟老师在stage3中的指点。余老师现在正写一本正则表达式的傻瓜书,请点击余晟老师的博客来探寻详情。



第一个不用这么麻烦拉:
1。由且仅由数字、字母(大小写)、特殊符号(@ % &…)组成,三者缺一不可,密码不少于8位
/^(?![0-9a-z]{8}|[@%&]{8})[a-z0-9@%&]{8}/i
2。十位的数字、字母组合密码,其中包含4位数字和6位字母。
我认为这类问题用正则虽然可以做到,但不是最好的方法,也不是最方便的方法。你给出的解我没有去分析它实际的对错,但是光从性能上讲就成问题的,lookahead和*的滥用,会导致匹配过程多次回溯,非常不好
比较好的方式是先用正则判断给定的字符串是否符合10位字母+数字的组成,然后再用字符串处理函数分别计算其中数字和字母出现的次数是否符合你的要求,这样会比较好。
[Reply]
@cnhacktnt
朋友,我猜你这个表达式思路大概对头,但形式不对,在Python里验证了一下,果然如此
(?![0-9a-z]{8}|[@%&]{8})
如果我没猜错,你的思路大致是:从起点之后,不能是都是数字或字母,也不能都是特殊符号
这两个条件,应当用(|)结构分隔;另外,其中每个分支都必须匹配“整个”字符串,而不能仅仅是“前八个字符”;
另外你的{}里只写了一个8,也就是说,密码“只能”为8位
我改写了一下大概是:
^(?!([0-9a-z]{8,}$|[@%&]{8,}$))[a-z0-9@%&]{8,}$
不知你觉得这样是否“麻烦”?
[Reply]
cnhacktnt Reply:
October 17th, 2009 at 8:19 pm
那两个条件我是用 | 来分隔的阿,在(?!)里加 | 不需要你再用括号,nest parenthesis 会增加额外开销。另外我把这个”密码不少于8位”看走眼了呵呵,这是我的失误。
正则是随手写的,没有经过验证呵呵。
重新改写:
/^(?![0-9a-z]*$|[@%&]*$)[a-z0-9@%&]{8}/i
最后的 $ 是可以不要的。
另外可能由于工作的关系,对于贪婪模式,个人非常抵触,如果这个正则用于实际项目的密码验证,大多数情况下用户提交的密码应该是有个范围限定的,比如8-16位。所以能有限定,还是把限定加上:
/^(?![0-9a-z]{8,16}$|[@%&]{8,16}$)[a-z0-9@%&]{8,16}$/i
[Reply]
cnhacktnt Reply:
October 17th, 2009 at 8:21 pm
另外,我第一篇回复中的”麻烦”没有恶意,sorry :-)
[Reply]
cnhacktnt Reply:
October 28th, 2009 at 11:51 pm
我的方法漏掉了一个环节,有逻辑上的漏洞(漏掉了全是字母和[@%&]这种情况),作如下修改:
/^(?![0-9a-z]{8,16}$|[a-z@%&]{8,16}$)[a-z0-9@%&]{8,16}$/i
感谢 rex 及 rex 朋友的指正。
很多时候,需要考虑多个条件的正则确实会让人发晕呵呵,这个时候,完整的测试数据覆盖就显得很重要:-)
[Reply]
@cnhacktnt
好,你下面改的更好!
如果可以的话,能给我一个“抵触贪婪模式”的实际例子吗?算是帮个忙
谢谢
[Reply]
cnhacktnt Reply:
October 23rd, 2009 at 5:17 pm
我的工作就是个例子,我的工作中是不允许出现 *, + 的
[Reply]
chunzi Reply:
October 25th, 2009 at 9:41 pm
如果用了 * + 就会被 charlie 认为:这个人显然不认识 never 这个英文单词!Slava 就是个活生生的例子。不过我们的工作写出来的 regex 普遍需要求值百万次/天以上,所以很在乎性能。cnhacktnt 显然抱有极高的专业素养。但这里对密码验证的情况,显然不是性能瓶颈,应该无所谓,问题的提出也多在探讨可能性。实践中最好分开判断,增加可读性:一行代码仅完成一个简单逻辑。更直观,也更易维护。:-)
[Reply]
第二道题的解法,手误了吧。
我用vim,第二道题的解法是:
^\(.*\d.*\d.*\d.*\d\)\@=\(.*[a-z].*[a-z].*[a-z].*[a-z].*[a-z].*[a-z]\)\@=[0-9a-z]\{10}$
[Reply]
rex Reply:
December 10th, 2009 at 9:55 pm
呵呵,向vim正则高手致敬!我一般不怎么用vim正则,觉得语法太怪异:)
[Reply]
第二题
^(?=(.*\d){4})(?=(.*[a-zA-Z]){6})[\da-zA-Z]{10}$
[Reply]
rex Reply:
December 20th, 2009 at 11:48 pm
验证通过。
[Reply]