本文目录导读:

在PHP中实现动态脱敏(Dynamic Data Masking)通常指在不修改原始数据的情况下,根据用户权限或场景动态展示掩码后的数据,以下提供几种常见实现方式:
基础脱敏函数(通用掩码规则)
class DataMasker {
/**
* 手机号脱敏:保留前3后4
*/
public static function mobile(string $mobile): string {
if (strlen($mobile) !== 11) {
return $mobile;
}
return substr_replace($mobile, '****', 3, 4);
}
/**
* 身份证脱敏:保留前4后4
*/
public static function idCard(string $id): string {
if (strlen($id) < 8) {
return $id;
}
return substr_replace($id, str_repeat('*', strlen($id) - 8), 4, -4);
}
/**
* 邮箱脱敏:用户名部分@前保留首字母,其余掩码
*/
public static function email(string $email): string {
$parts = explode('@', $email);
if (count($parts) !== 2) {
return $email;
}
$name = $parts[0];
$domain = $parts[1];
if (strlen($name) <= 2) {
$maskedName = substr($name, 0, 1) . str_repeat('*', strlen($name) - 1);
} else {
$maskedName = substr($name, 0, 1) . str_repeat('*', strlen($name) - 2) . substr($name, -1);
}
return $maskedName . '@' . $domain;
}
}
// 使用示例
echo DataMasker::mobile('13812345678'); // 138****5678
echo DataMasker::idCard('110101199001011234'); // 1101**********1234
echo DataMasker::email('testuser@gmail.com'); // t******r@gmail.com
基于策略模式的动态脱敏(支持配置)
interface MaskingStrategy {
public function mask($value): string;
}
class MobileStrategy implements MaskingStrategy {
public function mask($value): string {
return substr_replace($value, '****', 3, 4);
}
}
class EmailStrategy implements MaskingStrategy {
public function mask($value): string {
$parts = explode('@', $value);
$name = $parts[0];
$maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 2)) . substr($name, -1);
return $maskedName . '@' . ($parts[1] ?? '');
}
}
class DynamicMasker {
private array $strategies = [];
public function __construct(array $config = []) {
// 默认策略配置
$this->strategies = $config ?: [
'mobile' => new MobileStrategy(),
'email' => new EmailStrategy(),
];
}
public function maskField(string $field, $value): string {
if (isset($this->strategies[$field])) {
return $this->strategies[$field]->mask($value);
}
return $value; // 未配置策略则返回原值
}
public function maskArray(array $data, array $rules): array {
foreach ($rules as $field => $shouldMask) {
if ($shouldMask && isset($data[$field])) {
$data[$field] = $this->maskField($field, $data[$field]);
}
}
return $data;
}
}
// 使用示例
$masker = new DynamicMasker();
$user = [
'name' => '张三',
'mobile' => '13812345678',
'email' => 'zhangsan@example.com'
];
$masked = $masker->maskArray($user, [
'name' => false, // 不脱敏
'mobile' => true, // 脱敏
'email' => true // 脱敏
]);
// 结果: ['name' => '张三', 'mobile' => '138****5678', 'email' => 'z******n@example.com']
基于注解/AOP的自动脱敏(高级应用)
#[Attribute(Attribute::TARGET_PROPERTY)]
class Sensitive {
public string $type;
public function __construct(string $type = 'default') {
$this->type = $type;
}
}
class User {
#[Sensitive('name')]
public string $name;
#[Sensitive('mobile')]
public string $mobile;
#[Sensitive('email')]
public string $email;
public function __construct(string $name, string $mobile, string $email) {
$this->name = $name;
$this->mobile = $mobile;
$this->email = $email;
}
}
class SensitiveSerializer {
private array $maskers = [
'mobile' => [DataMasker::class, 'mobile'],
'email' => [DataMasker::class, 'email'],
'name' => function($name) {
return mb_substr($name, 0, 1) . str_repeat('*', mb_strlen($name) - 1);
}
];
public function serialize(object $object, array $context = []): array {
$reflection = new ReflectionClass($object);
$result = [];
foreach ($reflection->getProperties() as $property) {
$attributes = $property->getAttributes(Sensitive::class);
$value = $property->getValue($object);
if (!empty($attributes) && isset($this->maskers[$attributes[0]->newInstance()->type])) {
$masker = $this->maskers[$attributes[0]->newInstance()->type];
$result[$property->getName()] = $masker($value);
} else {
$result[$property->getName()] = $value;
}
}
return $result;
}
}
// 使用示例
$user = new User('张三', '13812345678', 'zhangsan@example.com');
$serializer = new SensitiveSerializer();
$maskedData = $serializer->serialize($user);
// 结果: ['name' => '张*', 'mobile' => '138****5678', 'email' => 'z******n@example.com']
数据库查询时动态脱敏(MySQL原生函数)
如果使用MySQL 8.0+,可以在SQL层面动态脱敏:
// MySQL查询时脱敏(示例查询)
$sql = "SELECT
id,
name,
CONCAT(LEFT(mobile,3), '****', RIGHT(mobile,4)) AS masked_mobile,
CONCAT(LEFT(email,1), '*****', SUBSTRING_INDEX(email, '@', -1)) AS masked_email
FROM users";
// 或者使用CASE WHEN进行权限控制
$sql = "SELECT
id,
name,
CASE WHEN :role = 'admin' THEN mobile ELSE CONCAT(LEFT(mobile,3), '****', RIGHT(mobile,4)) END AS mobile,
CASE WHEN :role = 'admin' THEN email ELSE CONCAT(LEFT(email,1), '****', SUBSTRING_INDEX(email, '@', -1)) END AS email
FROM users";
完整动态脱敏类(支持多种数据格式)
class DynamicDataMasker {
/**
* 根据类型掩码
*/
public static function mask($value, string $type = 'default'): string {
$value = (string)$value;
return match ($type) {
'mobile' => self::maskMobile($value),
'phone' => self::maskPhone($value),
'email' => self::maskEmail($value),
'id_card' => self::maskIdCard($value),
'bank_card' => self::maskBankCard($value),
'address' => self::maskAddress($value),
'name' => self::maskChineseName($value),
'password' => '******',
default => $value,
};
}
private static function maskMobile(string $mobile): string {
return strlen($mobile) === 11
? substr_replace($mobile, '****', 3, 4)
: $mobile;
}
private static function maskPhone(string $phone): string {
// 固定电话:保留区号和后4位
if (strlen($phone) >= 7) {
return substr($phone, 0, 4) . '****' . substr($phone, -4);
}
return $phone;
}
private static function maskEmail(string $email): string {
$parts = explode('@', $email);
if (count($parts) !== 2) return $email;
$name = $parts[0];
$domain = $parts[1];
$nameLen = strlen($name);
if ($nameLen <= 2) {
return substr($name, 0, 1) . str_repeat('*', $nameLen - 1) . '@' . $domain;
}
return $name[0] . str_repeat('*', $nameLen - 2) . $name[$nameLen - 1] . '@' . $domain;
}
private static function maskIdCard(string $id): string {
if (strlen($id) < 10) return $id;
return substr($id, 0, 4) . str_repeat('*', strlen($id) - 8) . substr($id, -4);
}
private static function maskBankCard(string $card): string {
if (strlen($card) < 8) return $card;
return substr($card, 0, 4) . ' **** **** ' . substr($card, -4);
}
private static function maskAddress(string $address): string {
// 只保留省市区,详细地址掩码
$pattern = '/(省|市|区|县)/u';
if (preg_match($pattern, $address, $matches, PREG_OFFSET_CAPTURE)) {
$pos = $matches[0][1] + mb_strlen($matches[0][0]);
return mb_substr($address, 0, $pos) . '****';
}
return $address;
}
private static function maskChineseName(string $name): string {
$len = mb_strlen($name, 'UTF-8');
if ($len == 1) return '*';
if ($len == 2) return mb_substr($name, 0, 1, 'UTF-8') . '*';
return mb_substr($name, 0, 1, 'UTF-8') . str_repeat('*', $len - 2) . mb_substr($name, -1, 1, 'UTF-8');
}
}
// 批量脱敏
class BatchDataMasker {
public static function maskBatch(array $data, array $fieldTypes): array {
$result = [];
foreach ($data as $key => $value) {
if (isset($fieldTypes[$key])) {
$result[$key] = DynamicDataMasker::mask($value, $fieldTypes[$key]);
} else {
$result[$key] = $value;
}
}
return $result;
}
}
// 使用示例
$userData = [
'name' => '张三丰',
'mobile' => '13912345678',
'email' => 'zhangsf@example.com',
'id_card' => '110101199001011234',
'address' => '北京市朝阳区建国路88号'
];
$fieldTypes = [
'name' => 'name',
'mobile' => 'mobile',
'email' => 'email',
'id_card' => 'id_card',
'address' => 'address'
];
$maskedData = BatchDataMasker::maskBatch($userData, $fieldTypes);
// 结果: ['name' => '张*丰', 'mobile' => '139****5678', ...]
最佳实践建议
- 配置驱动:将脱敏规则定义为配置文件,便于动态调整
- 权限分离:不同角色看到不同级别的脱敏数据
- 日志审计:记录脱敏前的原始数据操作日志
- 性能考虑:对大数组使用PDO的fetch模式或分批处理
- 国际化支持:处理多语言字符时使用mb_系列函数
- 防止过度脱敏:对短字符串(如2位数字)仅显示部分字符
选择哪种方案取决于你的具体需求:
- 简单项目:使用基础函数
- 复杂业务:使用策略模式+注解
- 高安全要求:结合数据库视图+ORM层面脱敏