敏感信息过滤实战:从正则匹配到AI识别的完整方案
先看效果,再看代码
上周我们线上突然报了个安全问题:用户在评论区贴了手机号,被爬虫抓走了。虽然不是我们的锅,但老板说“前端能不能拦一下”。我翻了翻历史代码,发现之前根本没做敏感信息过滤,纯靠后端清洗。这不行,得加一层前端兜底。
我折腾了半天,最后用正则 + 关键词匹配搞定了。核心逻辑其实就几行,但细节坑不少。下面直接上代码,你拿去就能用:
function filterSensitiveText(text) {
if (!text) return text;
// 手机号(11位,支持3-4-4或3-3-5分隔)
text = text.replace(/1[3-9]d(?:[-s]?d{4}){2}/g, '***');
// 身份证(15位或18位,末尾X大小写都处理)
text = text.replace(/(d{6})(d{8,10})(d{3}[0-9Xx]?)/g, '$1****$3');
// 邮箱(保留首尾字母,中间打码)
text = text.replace(/([a-zA-Z0-9._%+-])([a-zA-Z0-9._%+-]*)(@[a-zA-Z0-9.-]+.[a-zA-Z]{2,})/g, (_, start, middle, end) => {
if (middle.length <= 2) return start + '*'.repeat(middle.length) + end;
return start + '****' + end;
});
// 自定义关键词(比如公司内部系统名)
const keywords = ['内部系统', '测试账号', 'admin'];
keywords.forEach(word => {
const regex = new RegExp(word, 'gi');
text = text.replace(regex, '***');
});
return text;
}
这个函数我亲测有效,覆盖了大部分常见场景。调用也简单:const safeText = filterSensitiveText(userInput);
这个场景最好用
上面的函数适合用在用户输入实时预览或者提交前校验的场景。比如评论框、表单提交、聊天消息这些地方。我建议在 onInput 或 onChange 里跑一遍,但注意别影响性能——如果输入框特别长(比如富文本),得加个防抖。
另外,如果你用的是 Vue 或 React,别直接改原数据,而是生成一个“脱敏后的显示值”:
// Vue 示例
computed: {
maskedComment() {
return filterSensitiveText(this.rawComment);
}
}
// React 示例
const [rawText, setRawText] = useState('');
const maskedText = useMemo(() => filterSensitiveText(rawText), [rawText]);
这样原始数据还是干净的,提交时照样能发给后端(由后端做最终校验和存储),但页面上显示的是打码后的,避免无意泄露。
踩坑提醒:这三点一定注意
我一开始写完以为万事大吉,结果 QA 一测就崩了。总结三个血泪教训:
- 别在 replace 里直接写死星号数量:比如身份证,有人输 15 位,有人输 18 位,还有人手误多打空格。我最开始用
d{18}匹配,结果 15 位的漏掉了。后来改成d{8,10}中间段,才兼容。 - 邮箱正则别太激进:我见过用户写
a+b@test.com这种合法邮箱,但很多正则会把它截断。现在的写法保留了+和.,只模糊中间部分,实测更稳。 - 关键词匹配要区分大小写吗? 我们业务里“Admin”和“admin”都算敏感词,所以用了
gi标志。但如果你的关键词是“Apple”这种普通词,就得小心别误伤。建议关键词列表单独维护,别硬编码在函数里。
还有一个隐藏坑:**中文标点**。用户可能用全角括号、中文顿号分隔手机号,比如“138(1234)5678”。我的正则目前只处理了半角横杠和空格,如果你们业务需要,得自己扩展 [-su3000uFF08uFF09] 这类 Unicode 范围。
高级技巧:动态规则 + 性能优化
如果你们系统敏感词特别多(比如几百个),硬编码肯定不行。我后来搞了个动态加载方案:
// 从配置接口拉取敏感词库
async function loadSensitiveRules() {
const res = await fetch('https://jztheme.com/api/sensitive-rules');
const rules = await res.json();
return rules; // { phone: true, idCard: true, keywords: ['secret', 'confidential'] }
}
// 改造过滤函数
function createFilter(rules) {
return function(text) {
let result = text;
if (rules.phone) {
result = result.replace(/1[3-9]d(?:[-s]?d{4}){2}/g, '***');
}
if (rules.idCard) {
result = result.replace(/(d{6})(d{8,10})(d{3}[0-9Xx]?)/g, '$1****$3');
}
if (rules.keywords?.length) {
rules.keywords.forEach(word => {
const regex = new RegExp(word.replace(/[.*+?^${}()|[]\]/g, '\$&'), 'gi');
result = result.replace(regex, '***');
});
}
return result;
};
}
注意关键词转义那行:word.replace(/[.*+?^${}()|[]\]/g, '\$&')。这是必须的!否则用户如果关键词是“test*”,正则会直接报错。我当初就在这栽过,页面白屏了。
性能方面,如果规则不变,createFilter 只调用一次,生成闭包函数复用。比每次重新编译正则快多了。
最后说两句实在的
前端过滤敏感信息,本质是“防君子不防小人”。真想搞事的人绕过前端太容易了,所以后端必须做同样甚至更严格的校验。但前端这层能挡住 90% 的无心之失,比如用户复制粘贴时不小心带了隐私,或者测试账号误发到生产环境。
另外,别指望 100% 完美。比如银行卡号、车牌号这些,规则太复杂,而且不同国家格式不同。我建议优先处理手机号、身份证、邮箱这三个最高频的,其他按需补充。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 Web Worker 处理超长文本、用 trie 树优化关键词匹配),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论