本文目录导读:

- 核心思路
- 方法一:使用PHP函数或自定义脱敏函数(最通用)
- 方法二:使用Eloquent/Model的访问器(Laravel专属)
- 方法三:使用中间件统一拦截响应(前后端分离项目)
- 方法四:使用数据库视图或计算列(底层脱敏)
- 方法五:使用商业化数据脱敏库
- 生产环境最佳实践建议
- 简单对比表
在PHP项目中实现数据脱敏,常见的方法有以下几种,选择哪种取决于你的项目架构、性能要求以及脱敏的时机(存储前、查询后、展示时)。
核心思路
数据脱敏的黄金法则是:尽量在数据展示层(输出给用户时)进行脱敏,而不是在持久化层(数据库)修改原始数据。 除非业务有严格的合规要求(如PCI DSS要求存储时即为掩码形式)。
使用PHP函数或自定义脱敏函数(最通用)
这是最基础也是最灵活的方法,适用于任何框架。
<?php
/**
* 手机号脱敏:保留前3后4,中间用****代替
*/
function desensitizePhone(string $phone): string
{
if (strlen($phone) !== 11) {
return $phone; // 非标准手机号原样返回或抛出异常
}
return substr($phone, 0, 3) . '****' . substr($phone, -4);
}
/**
* 身份证号脱敏:保留前6后4,中间用********代替
*/
function desensitizeIdCard(string $idCard): string
{
if (strlen($idCard) !== 18) {
return $idCard;
}
return substr($idCard, 0, 6) . '********' . substr($idCard, -4);
}
/**
* 邮箱脱敏:用户名部分显示首字母+***,域名完整显示
* example@mail.com -> e***@mail.com
*/
function desensitizeEmail(string $email): string
{
$parts = explode('@', $email);
if (count($parts) !== 2) {
return $email;
}
$name = $parts[0];
$domain = $parts[1];
$maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 1));
return $maskedName . '@' . $domain;
}
// 使用示例
$user = [
'name' => '张三',
'phone' => '13800138000',
'idCard' => '110101199003071234',
'email' => 'zhangsan@example.com'
];
// 临时脱敏(仅在输出时)
$displayData = [
'name' => $user['name'],
'phone' => desensitizePhone($user['phone']),
'idCard' => desensitizeIdCard($user['idCard']),
'email' => desensitizeEmail($user['email']),
];
print_r($displayData);
优点:零依赖,高度可控。
缺点:每个地方都要手动调用,容易遗漏。
使用Eloquent/Model的访问器(Laravel专属)
如果你使用Laravel,可以在Model中定义访问器(Getter),每次获取属性时自动脱敏。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
// 定义一个脱敏后的手机号访问器
public function getPhoneMaskedAttribute()
{
$phone = $this->attributes['phone'] ?? '';
if (strlen($phone) === 11) {
return substr($phone, 0, 3) . '****' . substr($phone, -4);
}
return $phone;
}
// 定义身份证脱敏访问器
public function getIdCardMaskedAttribute()
{
$id = $this->attributes['id_card'] ?? '';
if (strlen($id) === 18) {
return substr($id, 0, 6) . '********' . substr($id, -4);
}
return $id;
}
// 你也可以覆盖原始phone属性的getter(谨慎使用,会改变所有获取phone的行为)
public function getPhoneAttribute($value)
{
// return $this->getPhoneMaskedAttribute();
// 如果想永久让phone属性返回脱敏值,取消注释
}
}
使用:
$user = User::find(1); echo $user->phone_masked; // 138****8000 echo $user->id_card_masked; // 110101********1234 echo $user->phone; // 原始号码(如果没有覆盖getter)
优点:与Laravel优雅集成,无需到处改代码。
缺点:只适用于Laravel项目。
使用中间件统一拦截响应(前后端分离项目)
如果你的项目是API接口,可以在HTTP响应中间件中自动处理所有敏感字段。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DesensitizeResponse
{
// 定义哪些字段需要脱敏及对应的脱敏规则
private array $rules = [
'phone' => 'maskPhone',
'id_card'=> 'maskIdCard',
'email' => 'maskEmail',
];
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// 只处理JSON响应,不处理重定向、文件等
if ($response->headers->get('Content-Type') === 'application/json') {
$data = json_decode($response->getContent(), true);
if (is_array($data)) {
$this->maskData($data);
$response->setContent(json_encode($data));
}
}
return $response;
}
private function maskData(array &$data): void
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
$this->maskData($value); // 递归处理嵌套
} elseif (isset($this->rules[$key])) {
$method = $this->rules[$key];
$value = $this->$method($value);
}
}
}
private function maskPhone($phone): string
{
return strlen($phone) === 11 ? substr($phone, 0, 3) . '****' . substr($phone, -4) : $phone;
}
private function maskIdCard($idCard): string
{
return strlen($idCard) === 18 ? substr($idCard, 0, 6) . '********' . substr($idCard, -4) : $idCard;
}
private function maskEmail($email): string
{
return preg_replace('/(?<=.).(?=.*@)/u', '*', $email);
}
}
注册中间件(在app/Http/Kernel.php中):
protected $middlewareGroups = [
'api' => [
// ...
\App\Http\Middleware\DesensitizeResponse::class,
],
];
优点:一键全局生效,后端代码完全不用改。
缺点:规则硬编码在中间件中,不够灵活;需要处理好深度嵌套情况。
使用数据库视图或计算列(底层脱敏)
直接在数据库层面创建脱敏视图,ORM查询视图而非原表。
MySQL示例:
CREATE VIEW masked_users AS
SELECT
id,
name,
CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
CONCAT(LEFT(email, 1), '***@', SUBSTRING_INDEX(email, '@', -1)) AS email
FROM users;
在Model中绑定视图:
class MaskedUser extends Model
{
protected $table = 'masked_users'; // 注意:这个Model是只读的
}
优点:脱敏逻辑下沉到数据库,性能高,任何访问方式都脱敏。
缺点:视图无法写入(除非使用触发器);变更脱敏规则需要修改数据库。
使用商业化数据脱敏库
一些PHP包提供了更高级的脱敏功能,如:
- mockery/mockery(测试用,但也可用于伪造数据)
- fakerphp/faker:生成脱敏后的假数据,常用于测试环境。
非脱敏而是替换:如果你需要在测试或开发环境中彻底替换真实数据(而不是局部掩码),Faker是更好的选择。
$faker = Faker\Factory::create(); echo $faker->phoneNumber; // 生成一个随机但格式合法的手机号
生产环境最佳实践建议
-
明确脱敏时机:
- 存储前脱敏:仅当法规明确要求(如身份证、银行卡号不应明文存储)。
- 查询后/展示前脱敏:推荐做法,保留原始数据用于内部统计或审计。
-
权限分级:
- 普通用户看到脱敏数据(如客服:13****8000)。
- 管理员/超级管理员看到完整数据(通过角色权限判断)。
- 实现方式:在访问器或中间件中加入权限检测。
-
日志脱敏:
确保打印日志时也做脱敏,避免敏感信息记录到日志文件。
-
测试覆盖:
对每个脱敏函数编写单元测试,确保边界情况(如空值、非标准格式)不报错。
-
性能考虑:
脱敏逻辑应轻量(避免正则回溯),对于高并发接口,考虑在数据库视图或缓存层处理。
简单对比表
| 方法 | 侵入性 | 灵活性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 自定义函数 | 高(到处调用) | 极高 | 高 | 小型项目或需要特殊逻辑 |
| Eloquent访问器 | 中(模型层) | 中 | 高 | Laravel项目 |
| 中间件 | 极低(自动) | 中 | 中 | 统一API响应脱敏 |
| 数据库视图 | 低(ORM换表) | 低 | 极高 | 多系统共享数据库 |
| Faker替换 | 低 | 高 | 高 | 测试环境 |
推荐组合:
- 开发/测试环境:使用Faker替代完整脱敏。
- 生产环境: Laravel项目用访问器 + 权限判断;非Laravel项目用中间件 + 自定义函数。
- 极高性能要求:使用数据库视图。