PHP 怎么PHP 动态脱敏

wen PHP项目 1

本文目录导读:

PHP 怎么PHP 动态脱敏

  1. 基础脱敏函数(通用掩码规则)
  2. 基于策略模式的动态脱敏(支持配置)
  3. 基于注解/AOP的自动脱敏(高级应用)
  4. 数据库查询时动态脱敏(MySQL原生函数)
  5. 完整动态脱敏类(支持多种数据格式)
  6. 最佳实践建议

在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', ...]

最佳实践建议

  1. 配置驱动:将脱敏规则定义为配置文件,便于动态调整
  2. 权限分离:不同角色看到不同级别的脱敏数据
  3. 日志审计:记录脱敏前的原始数据操作日志
  4. 性能考虑:对大数组使用PDO的fetch模式或分批处理
  5. 国际化支持:处理多语言字符时使用mb_系列函数
  6. 防止过度脱敏:对短字符串(如2位数字)仅显示部分字符

选择哪种方案取决于你的具体需求:

  • 简单项目:使用基础函数
  • 复杂业务:使用策略模式+注解
  • 高安全要求:结合数据库视图+ORM层面脱敏

抱歉,评论功能暂时关闭!