敏感信息过滤实战:从正则匹配到AI识别的完整方案

A. 俊娜 优化 阅读 1,194
赞 15 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周我们线上突然报了个安全问题:用户在评论区贴了手机号,被爬虫抓走了。虽然不是我们的锅,但老板说“前端能不能拦一下”。我翻了翻历史代码,发现之前根本没做敏感信息过滤,纯靠后端清洗。这不行,得加一层前端兜底。

敏感信息过滤实战:从正则匹配到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);

这个场景最好用

上面的函数适合用在用户输入实时预览或者提交前校验的场景。比如评论框、表单提交、聊天消息这些地方。我建议在 onInputonChange 里跑一遍,但注意别影响性能——如果输入框特别长(比如富文本),得加个防抖。

另外,如果你用的是 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 树优化关键词匹配),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论