CodeIgniter 数据脱敏应优先在模型层实现,通过封装可配置的辅助函数(如desensitize_mobile)处理手机号、身份证号,并用\_masked后缀区分脱敏字段,避免污染原始数据及日志泄露。

CodeIgniter 本身不内置数据脱敏功能,所有脱敏逻辑必须由开发者在模型层、服务层或视图层手动实现;依赖框架自动处理会漏掉关键字段,且容易在 select * 场景下完全失效。
脱敏该放在哪一层:模型层 vs 视图层 vs 自定义服务类
优先放在模型层(Model 类中),尤其是 get()、find() 或自定义查询方法内部。视图层脱敏看似简单,但会导致同一数据在不同模板中重复脱敏、JSON API 接口无法复用、以及无法对脱敏后字段做后续业务判断(比如“是否已脱敏”用于权限分流)。
- 模型层脱敏可统一控制,配合
$this->db->select()显式指定字段,避免select *带来意外暴露 - 若使用 Query Builder,可在
select()中嵌入 SQL 函数,如 MySQL 的CONCAT(LEFT(mobile,3),'****',RIGHT(mobile,4)),但注意这会让迁移至 PostgreSQL/SQL Server 时出错 - 不建议在视图里用
str_replace()或正则处理,因为 JSON 输出、API 返回、日志记录等场景都会绕过视图
手机号/身份证号脱敏的三种可行写法
不要硬编码 '****',要封装成可配置、可复用的工具方法。推荐在 app/Helpers/desensitize_helper.php 中定义:
-
desensitize_mobile($mobile):先校验是否为 11 位数字,再返回substr($mobile, 0, 3) . '****' . substr($mobile, -4) -
desensitize_idcard($idcard):区分 15 位/18 位,18 位取前 6 + 后 4,中间用str_repeat('*', 8)填充(不能固定写 8,否则 15 位身份证会错位) - 对数据库字段批量脱敏,可用
array_map()包裹结果集:array_map('desensitize_mobile', array_column($users, 'mobile'))
注意:如果字段可能为 null 或空字符串,所有脱敏函数开头必须加 if (empty($input)) return $input;,否则 substr(null, 0, 3) 会触发 Notice。
如何避免脱敏污染原始数据(特别是关联查询)
CodeIgniter 的 result() 返回的是对象数组,直接修改其中属性会影响后续使用 —— 比如你给 $user->mobile 赋了脱敏值,之后调用 send_sms($user->mobile) 就会发错号码。
- 永远用“副本”脱敏:生成新键名如
$user->mobile_masked,原字段$user->mobile保持不变 - 在模型中用
array_map()构造新数组,而非就地修改stdClass对象属性 - 若用
as_array(),可在select()阶段用别名:$this->db->select("id, name, mobile AS mobile_masked"),再配合 SQL 表达式脱敏(但跨库兼容性差)
最稳妥的方式是返回标准数组结构,脱敏字段明确带 _masked 后缀,让调用方清楚哪些是加工过的值。
JSON API 接口脱敏的坑:序列化器与输出时机
CodeIgniter 4 的 Response 类默认直接输出 json_encode(),此时脱敏必须在 return $this->response->setJSON($data) 之前完成。
- 不要依赖
JsonSerializable接口:CI4 的响应机制不触发该接口,写了也无效 - 避免在控制器里零散调用脱敏函数,应抽成
DesensitizeService::maskUserFields($user)统一入口 - 若用 RESTful 风格,建议在基类控制器的
after()方法中拦截$this->response->get_body()并重写,但仅适用于全量脱敏;细粒度字段控制仍需前置处理
真正容易被忽略的是日志记录环节:哪怕你在模型里做了完美脱敏,log_message('debug', print_r($user, true)) 仍可能把原始手机号打到日志文件里 —— 所有敏感字段在写日志前必须二次过滤。


















