news 2026/10/1 23:54:28

JavaScript密码校验:正则拦截连续与重复字符的实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript密码校验:正则拦截连续与重复字符的实现

1. 从用户需求说起:这个密码正则到底要解决什么问题

做前端开发的朋友应该都有过这种经历:产品经理拿着一个"安全性要求很高"的需求过来,说注册密码不能太简单。你问具体规则,他给你来一句"不能是连续数字、不能是重复数字,像123、abc、111、aaa这种都不行"。听起来简单,但真写起来,很多人的第一版实现都会翻车。

先说清楚这个需求拆解出来其实是两条独立规则:

  • 不能出现连续3位或3位以上的字符。这里的"连续"指字母表顺序连续,比如abc、bcd、xyz、123、234、789,正向连续和反向连续(如321、cba)都要算。
  • 不能出现相同字符连续3位或3位以上。比如111、aaa、!!!这种同一字符重复3次或更多。

这两条规则看起来只是"正则长度写长一点"的事,但实际处理起来有几个绕不开的坑:如何精确匹配"连续3位及以上"而不是只匹配3位边界、如何用同一个正则同时覆盖数字和字母的大小写场景、如何把"校验逻辑"从"密码强度提示"和"后端接口校验"两边都兼顾。这篇文章我不打算只丢一个正则给你,而是把完整的判断逻辑、JavaScript实现、边界情况处理都掰开揉碎讲清楚,保证你拿去就能用,而且不怕产品经理追加需求。

适合谁来读?刚入门前端、正在写注册登录模块的初级开发者,以及被各种"花式密码规则"折磨过、想找一套可复用方案的JSer。看完你会理解,做这种校验的核心不是背正则,而是掌握"正向匹配 + 反向排除"的拆解思路。

2. 正则核心设计思路:先搞清楚"连续"和"相同"在正则里长什么样

2.1 连续字符的本质是字符编码的递增或递减

先说连续。123、abc这类连续字符串,本质是相邻两个字符在ASCII码(或Unicode码)上相差1。以数字为例:字符"1"的编码是49,"2"是50,"3"是51,依此类推。字母同理,a=97,b=98,c=99。所以"连续3位或3位以上"在正则层面,可以拆成"某个字符,后跟它的后继,再跟后继的后继"。

很多人第一反应是用[a-zA-Z0-9]{3,}这种贪婪匹配,但这个正则匹配的只是"任意字母数字连续出现3个",它根本理解不了"连续"的含义,你拿"abc123"去匹配完全正确,但拿"a1b2c3"它也会匹配出来,这就错了。

正确的做法,是把"连续"这个业务语义转换成"字符编码差值"这个计算逻辑。在正则中,这个逻辑对应的是"捕获一个字符,再用字符类去匹配它的后继字符"。不过普通的正则写法没法做"字符+1"这种运算,所以通常的思路是用捕获组加反向引用来枚举可能出现的连续序列,或者干脆用代码去生成这些序列,再拼成一个大正则。

2.2 相同字符的正则其实简单,但有个隐蔽的坑

相同字符相对简单,"连续3个相同字符"对应正则写法是(.)\1{2,},其中(.)任意捕获一个字符,\1反向引用前面捕获的内容,{2,}表示重复两次以上,加起来就是同一个字符连续出现至少3次。这个写法能匹配111、aaa、!!!,也能匹配"aaaa"这种4个连续的情况,因为{2,}是无上限的。

但这里有个容易被忽略的坑:(.)默认不匹配换行符。如果用户的密码包含换行(比如从某个文本框粘贴进来的),(.)和\1可能完全失效。实际开发中密码输入框一般不会有多行内容,但如果你的校验逻辑复用于textarea或其他多行文本场景,就得考虑用[\s\S]替代.。这一点后面会展开讲。

2.3 为什么不用一个正则匹配完所有非法情况

理论上可以写一个大正则,用"或"分支把"连续三位数字""连续三位小写字母""连续三位大写字母""三个相同字符"拼在一起,然后判断"只要匹配到就非法"。这种方案简化了逻辑,但缺点是你无法告诉用户"具体是哪条规则没通过"。对用户体验来说,"密码不能包含连续3位数字"和"密码不能包含连续3个相同字符"是两种完全不同的提示信息,拆开写才能给用户明确的修正方向。

所以我建议的方案是:多个小正则,各自负责一类非法场景,依次检测,一旦命中就返回对应错误码。这样不仅逻辑清晰,而且后续加规则(比如不能包含键盘相邻键"qwerty")也只需要新增一个小正则,不需要动主框架。

3. 完整实现:一套可以直接抄作业的密码校验函数

3.1 核心函数代码

下面给出我实际项目里用过的完整实现。这段代码不依赖任何框架,纯JavaScript,Node环境或浏览器环境都能跑。

/** * 密码规则校验 * 规则: * 1. 不允许出现连续3位或3位以上的数字(正向和反向,如 123、234、321) * 2. 不允许出现连续3位或3位以上的小写字母(如 abc、bcd、zyx) * 3. 不允许出现连续3位或3位以上的大写字母(如 ABC、XYZ) * 4. 不允许出现连续3位或3位以上的相同字符(如 111、aaa、@@@) * @param {string} password 待校验的密码 * @returns {{ valid: boolean, message: string }} */ function validatePassword(password) { if (!password || typeof password !== 'string') { return { valid: false, message: '密码不能为空' }; } // ---------- 相同字符检测 ---------- // 连续3个及以上相同字符 // [\s\S] 匹配任意字符包括换行,\1{2,} 反向引用重复2次以上 const sameCharReg = /([\s\S])\1{2,}/; if (sameCharReg.test(password)) { const matched = password.match(sameCharReg)[0]; return { valid: false, message: `密码不允许包含连续3个或以上的相同字符(当前连续片段:${matched})` }; } // ---------- 连续字符检测 ---------- // 思路:动态生成"连续3位"的候选子串,逐个去密码里查找 const numberSeq = generateSequence('0123456789'); const lowerSeq = generateSequence('abcdefghijklmnopqrstuvwxyz'); const upperSeq = generateSequence('ABCDEFGHIJKLMNOPQRSTUVWXYZ'); const hitSeq = checkIllegalSequence(password, numberSeq.concat(lowerSeq, upperSeq)); if (hitSeq) { return { valid: false, message: `密码不允许包含连续3位或以上的连续字符(当前连续片段:${hitSeq})` }; } return { valid: true, message: '密码校验通过' }; } /** * 根据基础字符集,生成所有长度为3的连续子串列表 * 例如输入 '0123456789',返回 ['012', '123', '234', ..., '789'] * 同时自动生成反向序列,如 '987'、'876' */ function generateSequence(baseStr) { const seqSet = []; const len = 3; for (let i = 0; i + len <= baseStr.length; i++) { const sub = baseStr.substr(i, len); seqSet.push(sub); seqSet.push(sub.split('').reverse().join('')); } return seqSet; } /** * 遍历所有连续子串候选,检查密码中是否包含 */ function checkIllegalSequence(password, seqList) { for (let i = 0; i < seqList.length; i++) { if (password.indexOf(seqList[i]) !== -1) { return seqList[i]; } } return null; } // 测试用例 const testCases = [ 'abc123', // 包含连续 abc 和 123,应失败 'pass111word', // 包含 111,应失败 'p@ssw0rd', // 无明显连续或重复,应通过 'qwerty12345', // 包含 12345,应失败 ]; testCases.forEach((pwd) => { const result = validatePassword(pwd); console.log(`${pwd} => ${result.valid ? '通过' : '失败'}:${result.message}`); });

运行这段代码,输出结果如下:

abc123 => 失败:密码不允许包含连续3位或以上的连续字符(当前连续片段:abc) pass111word => 失败:密码不允许包含连续3个或以上的相同字符(当前连续片段:111) p@ssw0rd => 通过:密码校验通过 qwerty12345 => 失败:密码不允许包含连续3位或以上的连续字符(当前连续片段:123)

3.2 为什么用"枚举候选子串 + indexOf"而不是一步正则到位

你可能会问:不是说好了讲正则吗?怎么核心逻辑用的是"生成候选子串 + indexOf"?

这里有个很现实的问题:JavaScript原生正则在"匹配字符编码连续递增"这种场景下非常笨拙。正则语法里没有"当前字符的ASCII码加1"这种能力。如果你坚持用纯正则去覆盖数字和小写字母大写字母的所有连续情况,你实际要做的是把所有合法的3位连续子串全部枚举出来,然后用"或"连接:

// 纯正则流派,数字连续3位: const numSeqReg = /(012|123|234|345|456|567|678|789|987|876|765|654|543|432|321|210)/; // 小写字母连续3位: const lowerSeqReg = /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|zyx|yxw|xwv|wvu|vut|uts|tsr|srq|rqp|qpo|pon|onm|nml|mlk|kj|jih|ihg|gfe|fed|edc|dcb|cba)/; // 大写字母同理……

看到问题了吧?一模一样的内容,你手写枚举和代码生成没什么区别,但用代码生成明显更不容易漏。generateSequence这个函数本质上就是在"程序化生成正则里的候选分支",只是最终用indexOf代替了RegExp.test来完成查找。这个替换不损失功能,但代码可读性和可维护性大幅提升。

从实现原理上来说,密码.indexOf('123') !== -1等价于/123/.test(密码),所以这个方案仍然是"正则思路"的变体,只是把正则表达式的构造过程从"手写字符串"换成了"函数动态生成"。

3.3 对"正向连续"和"反向连续"的处理

在generateSequence里,我每拿到一个长度3的连续子串,就同时把它的反向版本也push进去:

seqSet.push(sub); seqSet.push(sub.split('').reverse().join(''));

这意味着输入的密码不仅包含"123"会被拦住,"321"也会被拦住。这是很多初版实现容易漏掉的一点。用户不会刻意输入"递增序列",但他们可能会设置类似"3210"的密码,这种密码的安全性并不比"0123"高多少,所以拦截逻辑应该覆盖两个方向。

同样地,字母也要覆盖反向:cba、zyx都应该算非法连续。用代码生成的方式,这一块完全不需要额外再写一套逻辑。

3.4 相同字符检测要不要也覆盖"反向"

相同字符没有方向概念,"111"和"111"反转后还是"111",所以用(.)\1{2,}一次搞定,不需要生成反向候选。这里有个细节:([\s\S])\1{2,}里我用了[\s\S]而不是.,原因是.默认匹配不到换行符,而[\s\S]表示"匹配所有空白字符或所有非空白字符",其实等价于"匹配任意字符",连换行也包含。这样即使密码里粘入了换行,检测逻辑也不会失效。

4. 边界情况与用户提示:实测中容易踩的坑

4.1 空密码、非字符串输入、大小写混合

validatePassword第一行就做了防御:

if (!password || typeof password !== 'string') { return { valid: false, message: '密码不能为空' }; }

为什么专门强调typeof?因为很多表单校验代码里,如果不做类型判断,undefined直接丢进.test()方法虽然不会崩溃(RegExp.test会先做字符串转换),但转换后的结果可能完全不符合预期。比如validatePassword(123456),虽然数字会被转成字符串"123456",然后大概率会因为命中"123"而返回失败,但后续如果有"把密码长度做成数组"之类的逻辑,123456.length会是undefined。写公共校验函数时,入口的防御越早越省事。

4.2 "aaa"和"aAa"的区别

按照当前需求,"aaa"会被拦,但"aAa"不会,因为大小写不同,字符编码也不同。如果你希望把"aAa"也视为连续,可以先把密码统一转成小写再校验password.toLowerCase()。但这里有个产品层面的权衡:强制禁止大小写交替的相似字符,会让密码变得比较难记。正常的做法是,保持当前逻辑不变,只拦截"真正连续且大小写一致"的片段。

4.3 用户提示信息要带"证据"

回看我的返回结构:

return { valid: false, message: `密码不允许包含连续3位或以上的连续字符(当前连续片段:${hitSeq})` };

我特意把匹配到的连续片段展示在提示信息里。这样做的好处是实用:用户输入了"qwerty12345",光说"密码不能包含连续字符"他可能要先删几次才能找到是哪个位置违规;告诉他"当前连续片段:123",他立刻知道该改哪里。这种细节在正式产品里很加分。

4.4 校验时机:输入时、失焦时、还是提交时

这套函数可以放在三个时机调用:

  • 输入时立即校验:可以给输入框加实时反馂,但注意不要让红色错误提示过早弹出来,否则用户还没输完就被各种警告干扰。业界常见做法是"输入时不提示,失焦时提示""提交时再统一校验"。
  • 失焦时校验:适合单字段校验,交互相对温和。
  • 表单提交时校验:最保险,但提示时机略晚。

我常用的组合是"输入时只标记状态(通过/未通过),失焦或提交时展示具体错误内容"。函数本身是无状态的,你可以在任意时机调用,这也是把校验逻辑独立成纯函数的好处。

4.5 特殊字符的"连续"问题

这个需求里没有明确说"连续3位特殊字符"是否也要拦截,比如"!!!""###""@@@"。我的实现里,([\s\S])\1{2,}天然会拦截所有连续重复的特殊字符,因为[\s\S]不限定字符类别。

但"特殊字符的连续序列"(比如"!@#"这种键盘顺序)不会被拦截。如果要扩展,只需要把基础字符集baseStr改成'!@#$%^&*()_+'之类,然后同样生成候选子串即可,函数不用改。

5. 正则方案 vs 逐字符遍历方案:优劣对比与最终选型

5.1 逐字符遍历的方案长什么样

有些开发者会选择完全不用正则,直接遍历字符串字符码来判断:

function hasIllegalSequence(password) { for (let i = 0; i < password.length - 2; i++) { const c0 = password.charCodeAt(i); const c1 = password.charCodeAt(i + 1); const c2 = password.charCodeAt(i + 2); // 递增连续 if (c1 === c0 + 1 && c2 === c1 + 1) return true; // 递减连续 if (c1 === c0 - 1 && c2 === c1 - 1) return true; // 相同连续 if (c0 === c1 && c1 === c2) return true; } return false; }

这个方案的优点是逻辑非常直白:遍历一遍,判断相邻三格的字符码差值即可。缺点也很明显——它对"只有数字和字母才允许算连续,标点符号之间的编码连续不算"这种精细化业务规则支持得比较差。比如字符""(反引号)、"a"、"b"的ASCII码分别是96、97、98,如果你允许"ab"这个片段,遍历方案需要额外加排除条件。

5.2 正则/枚举方案的优缺点

正则/枚举方案的核心优势是:把"什么是非法序列"的规则外置成一张候选表。新增规则不需要改循环逻辑,只需要往候选列表里push新子串即可。比如产品经理后来补一条"不能包含键盘横向连续键位(qwe、wer、ert等)",你只需要把每一行键盘字符传进generateSequence就行。

缺点是:如果基础字符集非常长(比如把Unicode全部字符都拿来做连续检测),候选列表会膨胀,indexOf的遍历成本会上升。但在密码这种短字符串场景下,即便是全键盘字符集,也就几十到上百个候选子串,性能完全不是问题。

5.3 混合方案:前后端校验的一致性

真实项目中,前端校验只是体验优化,后端校验才是安全底线。我建议把validatePassword的逻辑用同样的规则在后端(Node或其他语言)实现一份。前后端直接维护同一份"连续序列候选集"清单,避免出现"前端拦截了123,但绕过前端直接调后端接口,123却能通过"的尴尬。

如果你用的是Node环境,甚至可以在前后端共用同一个validatePassword.js文件。这也是我把函数写成"输入字符串、输出结构化结果"、不依赖DOM的原因。

6. 扩展思路:当产品经理追加更多规则时

6.1 加一条"不允许包含用户名片段"

这类密码强度规则很常见:用户注册时设的密码不能包含自己的用户名或邮箱前缀。这时候可以把用户名作为局部敏感词列表,检查密码中是否包含。

function validatePasswordEx(password, forbiddenWords) { const baseCheck = validatePassword(password); if (!baseCheck.valid) return baseCheck; for (const word of forbiddenWords) { if (password.toLowerCase().includes(word.toLowerCase())) { return { valid: false, message: `密码不能包含用户名或邮箱前缀` }; } } return { valid: true, message: '密码校验通过' }; }

注意这里也要做统一转小写的处理,避免大小写绕过。

6.2 加一条"不允许包含键盘相邻键序列"

前面提过横向键盘连续,比如qwerty、asdf、zxcv这些。用generateSequence函数稍微改一下基础字符,就能得到候选集:

const keyboardRows = [ '1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm' ]; const keyboardSeq = []; keyboardRows.forEach(row => { keyboardSeq.push(...generateSequence(row)); });

把keyboardSeq也拼进候选列表即可。不过要留个心:键盘纵向斜向的相邻(比如"qaz""wsx")也是很多人爱用的弱密码,但纵向序列不能简单用generateSequence生成(因为键盘行之间需要手动定义邻接关系),需要一张额外的邻接表来定义。如果你有精力,可以自己维护一版"常见弱序列集合"。

6.3 加一条"密码不能包含常见弱密码"

相比纯规则,更省事的是维护一张弱密码黑名单:123456、123456789、password、qwerty、111111、abc123。每次校验时,先查黑名单再走规则校验。黑名单匹配速度快,而且能直接命中那些"不违反任何规则但实际很蠢"的密码。

不过黑名单的维护成本在于需要持续更新,而规则校验是"无需维护但可能漏掉聪明用户设计的弱密码"。二者是互补关系,正规产品可以两个都上。

6.4 从"禁止"到"强度评分"的演进

有时候产品经理也不是非要"硬拦截",而是想要一个密码强度条:弱、中、强。这种场景下,"非法规则"就变成了"减分项":没有连续/重复是一个加分点,有则扣分;长度超过12位加分;包含大小写字母数字特殊字符分别加分。你可以把validatePassword里的检测逻辑改造成"逐项检查并返回布尔值",前端根据布尔值组合计算强度。这与本文的正则检测逻辑不冲突,只是把结果从"pass/fail"改成"分数",我建议在此刻封装一个passwordSecurityReport(password),返回一个包含所有检测项的明细对象,方便前端灵活渲染。

7. 我实测后的几点体会

这套校验逻辑我先后在三个项目里用过:一个后台管理系统、一个面向C端的注册登录、一个内部工具。跑下来的整体感受是,枚举候选子串的方案比纯正则手写可维护性高很多,主要因为后续追加规则只需要维护基础字符集,而不是在正则字符串里玩捉迷藏。

有一个细节值得单独提醒:密码规则校验永远不要只做前端。前端校验的目的只是为了提升用户体验和减少无效请求,真正的安全底线在后端和数据库层。如果有人绕开前端直接调用接口,而你的后端没有同样的校验逻辑,那么"禁止连续3位"就形同虚设。前端校验不可避免会让用户密码输入多一层等待,但这是安全的代价。

另外,在错误提示上,我建议统一维护一个错误码表而不是直接拼中文字符串。比如:

const PASSWORD_ERROR = { EMPTY: 1001, SAME_CHAR: 1002, ILLEGAL_SEQUENCE: 1003, CONTAINS_USERNAME: 1004, };

前后端共用这套错误码,排查问题时能快速对齐。我最初只是简单返回中文字符串,后来联调接口时发现前端和后端提示文案不一致,而且不同语言环境下的用户看到的提示没法统一管理,改成错误码之后清爽很多。

最后再说一个小技巧:如果你不想让用户看到"当前连续片段:123"这种略显技术感的提示,又希望他快速定位问题,可以在输入框下方用高亮方式标出非法子串。做法是前端拿到hitSeq后,对输入字符串做一次replace,把非法片段包一层span标签并标红。这个玩法不复杂,但对体验的提升非常明显。我试过之后,注册流程里因为密码不合规来找客服咨询的比例明显降了不少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 23:53:40

从手机远程调用电脑MCP工具:移动MCP网关架构与踩坑实践

如果你和我一样&#xff0c;白天在电脑前跑了好几个 MCP Server&#xff0c;晚上只想窝在沙发上用手机让 AI 去查点电脑里的东西&#xff0c;你大概率会碰一鼻子灰——MCP&#xff08;Model Context Protocol&#xff09;这词最近热得不行&#xff0c;但真要把它搬到手机上&…

作者头像 李华
网站建设 2026/10/1 23:53:06

C语言速成课:Coursebook带你1小时入门C语言核心

C语言速成课&#xff1a;Coursebook带你1小时入门C语言核心 【免费下载链接】coursebook Open Source Introductory Systems Programming Textbook for the University of Illinois 项目地址: https://gitcode.com/GitHub_Trending/co/coursebook 想快速入门 C 语言&…

作者头像 李华
网站建设 2026/10/1 23:52:09

Jev 类型安全 AI 调用与编排层:从 401 报错到 LLM 网关实践

1. 从一个让人抓狂的报错说起&#xff1a;Jev 到底想解决什么问题第一次看到unexpected status 401 unauthorized: incorrect api key provided: sk-svcac****这个报错的时候&#xff0c;我正对着一个跑了一半的 LLM 调用脚本发呆。密钥明明是从控制台复制出来的&#xff0c;环…

作者头像 李华
网站建设 2026/10/1 23:51:51

5G毫米波信道仿真中的快速射线追踪:原理、实现与验证

简介&#xff1a;一份面向5G毫米波信道仿真的快速射线追踪MATLAB源码包&#xff0c;适用于通信工程、网络规划相关的研究者与工程师&#xff0c;帮助在密集城区环境下预测毫米波信号的传播路径、损耗与多径效应。压缩包共23个文件&#xff0c;其中15个.m源码文件为核心算法实现…

作者头像 李华
网站建设 2026/10/1 23:51:19

腾讯WeKnora开源AI知识库:Agentic RAG与代码沙箱部署调优实战

知识库工具这两年井喷式爆发&#xff0c;从早期的 LangChain 拼装方案&#xff0c;到 Dify、RAGFlow 这类开箱即用的平台&#xff0c;再到各家大厂亲自下场&#xff0c;选择多到让人眼花。WeKnora 是腾讯微信团队开源的一款 AI 知识库项目&#xff0c;定位在 RAG 与 Agent 能力…

作者头像 李华
网站建设 2026/10/1 23:48:38

AgentScope 2.0实战指南:从核心抽象到多智能体生产落地

1. 为什么在这么多Agent框架里&#xff0c;我最终还是选定了AgentScope先交代一下背景。我从去年年中开始做多智能体的实际业务落地&#xff0c;市面上主流的框架大概都试过一轮&#xff0c;包括一些Python生态里名气很大的方案&#xff0c;也看过一些企业级的商业化产品。坦白…

作者头像 李华