本文目录导读:

在ThinkPHP项目中实现敏感数据脱敏展示,主要分为数据库层脱敏和展示层脱敏两种思路,下面我会从配置方式、核心代码实现、以及自动脱敏机制三个维度,给你一套完整的落地方案。
核心思路与工具类封装
为了不污染原有的业务代码,建议封装一个脱敏工具类,支持多种脱敏规则(手机号、身份证、银行卡、邮箱、姓名等)。
文件位置: app/common/library/Desensitization.php
<?php
declare(strict_types=1);
namespace app\common\library;
/**
* 数据脱敏工具类
*/
class Desensitization
{
/**
* 手机号脱敏:保留前3后4
* 138****5678
*/
public static function mobile(string $mobile): string
{
if (empty($mobile)) return '';
return substr_replace($mobile, '****', 3, 4);
}
/**
* 身份证脱敏:保留前6后4
* 110101********1234
*/
public static function idCard(string $idCard): string
{
if (strlen($idCard) < 10) return $idCard;
return substr_replace($idCard, str_repeat('*', 8), 6, -4);
}
/**
* 银行卡脱敏:保留前4后4
* 6222 **** **** 1234
*/
public static function bankCard(string $bankCard): string
{
if (strlen($bankCard) < 8) return $bankCard;
$masked = substr_replace($bankCard, str_repeat('*', 4), 4, -4);
// 按4位分组便于阅读
return implode(' ', str_split($masked, 4));
}
/**
* 邮箱脱敏:保留首字母和@后域名
* z***@qq.com
*/
public static function email(string $email): string
{
if (empty($email) || !str_contains($email, '@')) return $email;
[$prefix, $domain] = explode('@', $email, 2);
if (strlen($prefix) <= 2) {
$maskedPrefix = substr($prefix, 0, 1) . '***';
} else {
$maskedPrefix = substr($prefix, 0, 1) . str_repeat('*', strlen($prefix) - 1);
}
return $maskedPrefix . '@' . $domain;
}
/**
* 姓名脱敏:保留姓氏,名用*代替
* 张*
*/
public static function name(string $name): string
{
if (mb_strlen($name) <= 1) return $name . '*';
// 保留第一个字符
return mb_substr($name, 0, 1) . str_repeat('*', mb_strlen($name) - 1);
}
/**
* 地址脱敏:只保留前三字和后三字
*/
public static function address(string $address): string
{
if (mb_strlen($address) <= 6) return $address;
return mb_substr($address, 0, 3) . '****' . mb_substr($address, -3);
}
/**
* 万能脱敏:根据类型自动选择
*/
public static function mask(string $type, string $value): string
{
if (empty($value)) return '';
return match (strtolower($type)) {
'mobile' => self::mobile($value),
'id_card' => self::idCard($value),
'bank_card' => self::bankCard($value),
'email' => self::email($value),
'name' => self::name($value),
'address' => self::address($value),
default => $value,
};
}
}
模型层自动脱敏(推荐方案)
如果你使用的是 ThinkPHP 6/8,可以通过模型 获取器(Getter)实现自动脱敏,这样,只要查询该模型的数据,敏感字段就会自动处理,无需在控制器中手动调用。
使用步骤:
- 在模型类中引入 Trait(或直接写方法)
<?php
declare(strict_types=1);
namespace app\common\model;
use think\Model;
use app\common\library\Desensitization;
class User extends Model
{
// 需要脱敏的字段及规则映射
protected $desensitizationMap = [
'mobile' => 'mobile',
'id_card' => 'id_card', // 注意:这里写的是数据库字段名
'bank_no' => 'bank_card',
'email' => 'email',
'real_name'=> 'name',
];
// 自动调用获取器
public function getMobileAttr($value): string
{
return Desensitization::mobile($value);
}
public function getIdCardAttr($value): string
{
return Desensitization::idCard($value);
}
public function getBankNoAttr($value): string
{
return Desensitization::bankCard($value);
}
public function getEmailAttr($value): string
{
return Desensitization::email($value);
}
public function getRealNameAttr($value): string
{
return Desensitization::name($value);
}
// 如果想实现更加通用的动态脱敏,可以覆盖 toArray 方法
// 但该方法优先级低于具体字段的获取器,这里建议用上面的具体方法
}
注意:ThinkPHP 的获取器
getXxxAttr默认是在输出时(toArray())自动调用的,这正好符合脱敏展示的需求,注意如果数据库中的字段为空,记得处理 null。
控制器/服务层手动脱敏(灵活方案)
如果不想影响模型内部数据(例如需要保留原始数据用于内部逻辑),可以使用这个方案,通过一个基类控制器进行统一处理。
示例(在控制器中):
<?php
declare(strict_types=1);
namespace app\controller;
use app\common\library\Desensitization;
class UserController extends BaseController
{
public function index()
{
$users = User::select()->toArray();
// 手动遍历脱敏
foreach ($users as &$user) {
$user['mobile'] = Desensitization::mobile($user['mobile']);
$user['id_card'] = Desensitization::idCard($user['id_card']);
}
return json(['data' => $users]);
}
}
日志与调试的脱敏(防止敏感信息泄露)
在某些场景下,我们需要打印日志(如第三方API请求日志),此时建议记录脱敏后的数据。
use app\common\library\Desensitization;
// 在日志写入之前脱敏
$logData = [
'mobile' => Desensitization::mobile($params['mobile']),
'password' => '***',
'real_name' => Desensitization::name($params['real_name']),
];
trace($logData, 'user_request');
前端脱敏(补充方案)
如果后端接口需要给前端返回原始数据(用于编辑回显),但前端展示时需要脱敏,可以在模型中使用 hidden 或 visible 配合 append 字段来实现双字段返回。
// 在模型中定义一个脱敏字段(非数据库字段)
public function getMobileMaskedAttr($value, $data): string
{
return Desensitization::mobile($data['mobile'] ?? '');
}
在控制器中:
$user = User::find(1); // 返回原始 mobile 字段,同时附加一个 mobile_masked 脱敏字段供前端展示 $result = $user->append(['mobile_masked'])->toArray(); // 或者隐藏原始字段:$user->hidden(['mobile'])->append(['mobile_masked'])->toArray();
总结建议
| 场景 | 推荐方案 |
|---|---|
| API接口返回数据 | 模型获取器 (方案二) 或 手动调用工具类 (方案三) |
| 后端日志记录 | 调用工具类 (方案四) |
| 前端编辑回显 + 列表展示 | 双字段返回 (方案五) |
| 支持多种复杂脱敏规则 | 封装通用工具类 (方案一) |
核心建议: 优先使用模型层的获取器机制,它的侵入性最低,且能全局生效,避免在控制器中漏写脱敏逻辑,如果你的项目对原始数据的完整性要求极高(例如需要在后端进行计算),那么建议使用“双字段返回”方案,将原始字段和脱敏字段同时返回,前端按需使用。