敏感词过滤应按场景选型:模型观察者适用于数据库写入前统一拦截,中间件适用于请求参数实时校验;需用Aho-Corasick算法替代str_contains,预编译Trie树并带版本号缓存词库。

敏感词过滤该用中间件还是模型观察者
取决于你过滤的触发时机和数据流向。如果所有写入数据库的内容都要过审(比如用户发帖、评论),模型观察者更稳妥;如果只是 API 请求体里某些字段要实时拦截(比如提交表单时校验标题),中间件更轻量、响应更快。
常见错误现象:AppServiceProvider 里硬编码过滤逻辑,导致无法复用、难测试;或在控制器里重复写 str_contains(),漏掉更新场景。
- 模型观察者适合:需要持久化前拦截,且规则统一(如所有
Post、Comment模型都走同一套词库) - 中间件适合:只校验请求参数、不涉及模型状态,或需提前返回 422 错误(如
ValidateSensitiveWords::class) - 避免在
boot()方法里加载整张敏感词表到内存——词库超 5000 条时,每次请求都DB::table('sensitive_words')->get()会拖慢响应
Laravel 中如何高效匹配敏感词(不是简单 str_contains)
str_contains() 只能做子串匹配,无法处理“苹果”误伤“苹果手机”或漏掉“苹 果”(含空格)这类变体。真正在生产环境用,得上多模匹配或前缀树(Trie)。
性能影响明显:10 万条敏感词 + 单次 500 字内容,暴力遍历匹配可能耗时 >200ms;用 php-aho-corasick 扩展可压到 5ms 内。
- 推荐用
php-aho-corasick库(支持中文、忽略空白符、可配置模糊等级) - 词库必须预编译成 Trie 树,不要每次请求都重建——放在
app/Services/SensitiveWordFilter.php的静态属性里初始化一次 - 注意 Laravel 的
config('app.debug')开启时,别让词库自动重载,否则热更新会频繁重建树,CPU 突增 - 示例调用:
$filter->find($content)返回命中词数组,而非布尔值,方便记录日志或替换
替换策略怎么选:星号屏蔽、脱敏、还是直接拒绝
不能一刀切。运营要求不同,技术实现也得灵活。直接 throw new ValidationException 最省事,但用户体验差;全量星号替换(如“***”)可能破坏语义,导致审核误判。
容易踩的坑:用 str_replace() 替换多次,结果把“黑客”替成“黑**”,再替“黑”变成“***”,越替越乱。
- 必须按词长度倒序替换(先替“黑客技术”,再替“黑客”),否则短词污染长词
- 推荐用
preg_replace_callback()+ 预编译正则(词库导出为|分隔的 pattern),一次扫描完成 - 对评论类内容建议“标记不发布”,而非自动替换——留人工复核入口;对搜索关键词可允许部分匹配但降权
- 替换后保留原始字符长度(如“苹果”→“**”),避免前端排版错乱
缓存敏感词库时要注意什么
缓存不是加个 Cache::remember 就完事。词库更新频率低,但一旦更新,所有 worker 进程必须同步失效,否则出现“有的机器已更新,有的还在用旧词库”的脏数据问题。
常见错误:用文件缓存(file driver)+ 多台服务器,导致各节点词库不一致;或 Redis 缓存没设 version key,改词库后清不干净。
- 必须带版本号缓存,比如键名用
sensitive_words:v2,version 存在数据库或配置中心 - 更新词库时,先改 version,再删旧缓存,两步操作要原子——用 Redis 的
pipeline或数据库事务兜底 - 不要缓存「过滤结果」,只缓存「词库结构」。内容千变万化,缓存结果毫无意义还占内存
- 本地开发用
arraydriver 没问题,上生产必须切到redis或memcached
最麻烦的其实是词库本身的维护:同音词、拼音缩写、火星文、中英文混写——这些没法靠代码穷举,得靠运营后台+人工标注反馈闭环。技术能做的,是把匹配链路跑稳、别让一条脏数据漏进数据库。











