敏感词过滤需分层选型:小项目用str_replace,词库超300条或QPS超50须用DFA树;TP6中stripos易因BOM、全角空格、未倒序匹配等失效;DFA树须UTF-8安全拆解、常驻内存、缓存带md5校验;策略应分级(拦截/打标),配合微信msgSecCheck并严格解析suggest字段。

敏感词过滤在 ThinkPHP 项目里不是“加个正则就能跑”,而是必须按数据规模、QPS、文本长度、更新频率分层选型——小项目用 str_replace 没问题,但一旦词库超 300 条或接口 QPS 过 50,暴力遍历就会吃满 CPU,且漏匹配、误替换频发。
为什么 stripos + foreach 在 TP6 里特别容易翻车
ThinkPHP 6 的请求生命周期中,$_POST 或 $request->param() 返回的值可能已被自动转义(如 ")、JSON 解析过、或含 BOM/全角空格;而 stripos 是字节级匹配,对 UTF-8 中文不安全:
- 数据库读出的敏感词带 UTF-8 BOM 头,
trim()清不掉,stripos('微信', '微信')返回false - 用户输入 “shǎbī” 或 “傻@逼”,原始
stripos完全无法命中 - 词库含 “苹果” 和 “苹果手机”,没按长度倒序排列,先替换了 “苹果”,剩下 “手机” 被漏判
- 中间件里直接遍历
$_POST,遇到 JSON 字段(如{"content":"..."})会把整个字符串当关键词去扫,崩溃或误杀
TP6 中间件集成 DFA 树的实操要点
别写行为类(Behavior),TP6 已废弃该机制;中间件是唯一能统一拦截所有输入入口的位置。关键不是“有没有 DFA”,而是树能不能常驻内存、是否支持 UTF-8 安全切分:
- 建树时必须用
mb_substr($word, $i, 1, 'UTF-8')单字符拆解,禁用str_split或裸substr,否则汉字变乱码 - 树结构缓存到
think\Cache或 APCu,key 建议带词库 md5,比如'sensitive_trie_' . md5_file(config_path('sensitive_words.txt')),避免热更新失效 - 中间件中不要每次 new 实例,改用容器单例:
app()->make(SensitiveDfaFilter::class)->search($content) - 对 JSON 请求,先
json_decode($request->getContent(), true),再递归过滤字符串字段,跳过file、avatar_url等非文本键
替换、拦截、还是打标?策略得可配
硬拦截(throw new ValidateException)适合注册昵称等强校验场景;但评论、弹幕这类高频内容,应允许运营配置策略:
立即学习“PHP免费学习笔记(深入)”;
- 命中低风险词(如“抽奖”“送礼”)只加
is_flagged = 1字段入库,进人工复审队列 - 高风险词(如“法轮功”“邪教”)必须拦截,返回 HTTP 400 + 明确提示:“内容包含违规词汇,请修改后提交”
- 前端 JS 只做轻量提示(用高频词白名单 +
indexOf),绝不传完整词库或匹配逻辑到浏览器 - 所有命中记录写入日志表(
sensitive_logs),含user_id、content_hash、matched_words、created_at,供审计与词库优化
本地过滤 + 微信 API 协同才是小程序过审关键
微信小程序内容安全审核失败,90% 不是因为没调 msgSecCheck,而是本地漏过滤 + API 返回没判 suggest:
- 本地 DFA 必须先过一遍,过滤掉明确违规词;再调微信接口,避免被限流或触发风控降权
- 微信返回必须检查
$res['result']['suggest'],不是errcode === 0就放行:"pass"才可展示,"review"需进人工池,"risky"必须拦截 - 图片检测前,确保路径是服务端绝对路径(如
runtime/upload/xxx.jpg),且文件 ≤1MB;GIF 动图需先imagecreatefromgif→imagejpeg转静态 -
access_token必须用Cache::remember('wx_access_token', 7200, ...)缓存,否则 2 小时内超 2000 次调用会返回errcode: 40001
真正难的不是写一个能跑的过滤器,而是让树结构不随每次请求重建、让 UTF-8 切分不出错、让词库更新后缓存自动失效、让微信返回的 suggest 被业务代码真正消费——这些细节卡住,线上就不是“有无过滤”,而是“有没有效”。



















