thinkphp 不内置不可见字符过滤,需手动清洗;input() 仅做映射与类型转换,不清理内容;推荐在控制器或验证器中对字符串字段用正则去除 bom、零宽、控制符等。

ThinkPHP 本身不提供内置的“不可见字符过滤”机制,所有不可见字符(如零宽空格 \u200b、字节顺序标记 \ufeff、控制字符 \x00–\x08、\x0b\x0c\x0e\x0f、\x10–\x1f 等)都会原样进入 input()、$_POST、$_GET,最终可能污染数据库、破坏 JSON 解析、触发 XSS 或导致前端渲染异常。
为什么不能依赖 input() 自动清理
input() 只做键名映射和类型转换,不做内容清洗;它甚至不会剥离首尾空白(trim() 都要你自己加)。更关键的是:TP6 的 think\Request 在构造时已将原始输入流转为字符串,不可见字符此时已是合法 UTF-8 码点,框架无从识别其“语义无效性”——它只认编码合法,不认用途合理。
常见后果包括:
- 用户粘贴富文本时带入
\u200b,导致表单校验通过但数据库唯一索引冲突(看似相同昵称,实则末尾有零宽) - Excel 导入文本含
\r\n混合\n+ 隐藏\x00,json_encode()直接返回false - API 接收 JSON 字符串,开头有 BOM(
\ufeff),json_decode()失败且不报具体位置
在控制器中对指定字段做预处理
最可控的做法是在接收后、验证前手动清洗。不要全局替换,避免误伤合法用途(如密码允许含控制符,或某些协议需保留特定分隔符)。
推荐清洗逻辑(放在控制器方法开头):
use think\facade\Input;
$data = Input::post();
// 清洗文本类字段,跳过文件、数组、数字等
foreach (['title', 'content', 'nickname', 'remark'] as $field) {
if (isset($data[$field]) && is_string($data[$field])) {
// 去 BOM、零宽、控制符(\x00-\x08, \x0b-\x0c, \x0e-\x1f)
$data[$field] = preg_replace('/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ufeff\u200b-\u200f\u202a-\u202f\u2060-\u206f\uf900-\ufaff]/u', '', $data[$field]);
// 可选:合并连续空白为单个空格,并 trim
$data[$field] = preg_replace('/\s+/', ' ', trim($data[$field]));
}
}
注意:
- 正则中的
/u修饰符必须加,否则 Unicode 字符匹配失效 - 不要用
strip_tags()替代——它不处理零宽、BOM 等非 HTML 字符 - 若字段需保留换行,把
\n、\r从正则中移出;但建议统一归一化为\n(参考换行符处理文档)
在验证器中封装可复用的清洗规则
把清洗逻辑下沉到验证器,既复用又符合 TP 的数据流习惯。定义一个自定义验证规则:
// app/validate/common/StrClean.php
<?php namespace app\validate\common;
use think\Validate;
class StrClean extends Validate
{
protected $rule = [
'value' => 'require|alphaNumDash|length:1,255',
];
protected $message = [
'value.require' => '内容不能为空',
];
// 自定义场景:clean_text 清洗不可见字符
public function sceneCleanText($scene = [])
{
return $this->append('value', 'callback:cleanInvisible');
}
protected function cleanInvisible($value, $rule, $data, $field)
{
$cleaned = preg_replace('/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ufeff\u200b-\u200f\u202a-\u202f\u2060-\u206f\uf900-\ufaff]/u', '', $value);
if ($cleaned !== $value) {
// 记录被清洗行为(仅开发/日志需要)
\think\facade\Log::info('Invisible chars cleaned in field '.$field, ['original' => $value, 'cleaned' => $cleaned]);
}
return $cleaned;
}
}
使用时:
$validate = new \app\validate\common\StrClean(); $result = $validate->sceneCleanText()->check($data);
这样既保持验证入口统一,又能确保清洗动作不被遗漏。
JSON 请求体开头含 BOM 怎么办
当客户端 POST 一个带 BOM 的 UTF-8 JSON(如某些旧版编辑器导出),php://input 读出来是 \ufeff{...},直接 json_decode() 返回 null 且 json_last_error_msg() 是 “Syntax error”,难以定位。
必须在解析前剥离:
$raw = file_get_contents('php://input');
if (substr($raw, 0, 3) === "\xef\xbb\xbf") {
$raw = substr($raw, 3);
}
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \think\Exception('Invalid JSON: ' . json_last_error_msg());
}
关键点:
- BOM 是字节序列,不是 Unicode 字符,不能用
mb_函数判断 - 必须用
substr($raw, 0, 3)判断,不能用strpos($raw, "\ufeff")—— 后者在 UTF-8 字符串里查 Unicode 码点,而 BOM 还未解码 - 此逻辑应放在中间件或 Base Controller 的
initialize()中,避免每个接口重复写
真正麻烦的不是“怎么删”,而是“删哪些”——不同业务对“不可见”的定义不同。比如客服系统要保留 \u200e(左向隐式),而评论系统必须干掉所有零宽。清洗策略得按字段定,别图省事一刀切。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











