PHP数据脱敏显示方案

wen PHP项目 1

本文目录导读:

PHP数据脱敏显示方案

  1. 通用脱敏函数
  2. 基于注解的脱敏方案
  3. 中间件脱敏方案(Laravel示例)
  4. 数据库查询时脱敏(SQL方案)
  5. 自定义脱敏配置(配置文件方式)
  6. 完整的脱敏工具类(包含所有常用类型)
  7. 总结与建议

我来介绍几种PHP数据脱敏显示的方案,包括常见的数据类型脱敏方法。

通用脱敏函数

<?php
class DataMasking
{
    /**
     * 手机号脱敏
     * 保留前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 $idCard): string
    {
        if (strlen($idCard) < 8) {
            return $idCard;
        }
        return substr_replace($idCard, '**********', 4, 10);
    }
    /**
     * 姓名脱敏
     * 保留姓,名字用*代替
     */
    public static function name(string $name): string
    {
        if (mb_strlen($name) <= 1) {
            return $name;
        }
        $first = mb_substr($name, 0, 1);
        $rest = str_repeat('*', mb_strlen($name) - 1);
        return $first . $rest;
    }
    /**
     * 邮箱脱敏
     * 保留前3位和域名
     */
    public static function email(string $email): string
    {
        $atPos = strpos($email, '@');
        if ($atPos === false) {
            return $email;
        }
        $username = substr($email, 0, $atPos);
        $domain = substr($email, $atPos);
        if (strlen($username) <= 3) {
            return '***' . $domain;
        }
        return substr($username, 0, 3) . str_repeat('*', strlen($username) - 3) . $domain;
    }
    /**
     * 银行卡号脱敏
     * 保留前4位和后4位
     */
    public static function bankCard(string $cardNo): string
    {
        if (strlen($cardNo) <= 8) {
            return $cardNo;
        }
        $masked = str_repeat('*', strlen($cardNo) - 8);
        return substr($cardNo, 0, 4) . $masked . substr($cardNo, -4);
    }
    /**
     * 地址脱敏
     * 保留前6个字符
     */
    public static function address(string $address): string
    {
        if (mb_strlen($address) <= 6) {
            return $address;
        }
        return mb_substr($address, 0, 6) . str_repeat('*', mb_strlen($address) - 6);
    }
    /**
     * IP地址脱敏
     */
    public static function ip(string $ip): string
    {
        $parts = explode('.', $ip);
        if (count($parts) !== 4) {
            return $ip;
        }
        return $parts[0] . '.*.*.' . $parts[3];
    }
    /**
     * 通用脱敏方法
     * @param string $value 原始值
     * @param string $type 类型:mobile, email, name, id_card, bank_card, address, ip
     */
    public static function mask(string $value, string $type): string
    {
        switch ($type) {
            case 'mobile':
                return self::mobile($value);
            case 'email':
                return self::email($value);
            case 'name':
                return self::name($value);
            case 'id_card':
                return self::idCard($value);
            case 'bank_card':
                return self::bankCard($value);
            case 'address':
                return self::address($value);
            case 'ip':
                return self::ip($value);
            default:
                return $value;
        }
    }
}
// 使用示例
echo DataMasking::mobile('13812345678');  // 138****5678
echo DataMasking::name('张三丰');          // 张**
echo DataMasking::email('test@example.com'); // tes*****@example.com

基于注解的脱敏方案

<?php
/**
 * 脱敏注解
 */
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MaskAttribute
{
    public function __construct(
        public string $type = 'default',  // 脱敏类型
        public ?int $start = null,        // 开始位置
        public ?int $length = null,       // 脱敏长度
        public string $maskChar = '*',    // 掩码字符
        public bool $keepEnd = true       // 保留末尾字符
    ) {}
}
/**
 * 脱敏处理器
 */
class MaskHandler
{
    public static function handle(object $object)
    {
        $reflection = new \ReflectionClass($object);
        foreach ($reflection->getProperties() as $property) {
            $attributes = $property->getAttributes(MaskAttribute::class);
            if (empty($attributes)) {
                continue;
            }
            $maskAttribute = $attributes[0]->newInstance();
            $property->setAccessible(true);
            $value = $property->getValue($object);
            if ($value !== null) {
                $maskedValue = self::maskValue($value, $maskAttribute);
                $property->setValue($object, $maskedValue);
            }
        }
    }
    private static function maskValue($value, MaskAttribute $attr): string
    {
        if ($attr->start !== null) {
            return self::maskPart($value, $attr->start, $attr->length, $attr->maskChar, $attr->keepEnd);
        }
        // 根据类型脱敏
        switch ($attr->type) {
            case 'mobile':
                return DataMasking::mobile($value);
            case 'name':
                return DataMasking::name($value);
            case 'email':
                return DataMasking::email($value);
            case 'id_card':
                return DataMasking::idCard($value);
            default:
                return (string)$value;
        }
    }
    private static function maskPart($value, int $start, ?int $length, string $maskChar, bool $keepEnd): string
    {
        $str = (string)$value;
        $len = strlen($str);
        if ($start >= $len) {
            return $str;
        }
        $maskLen = $length ?? ($keepEnd ? $len - $start - 1 : $len - $start);
        if ($maskLen <= 0) {
            return $str;
        }
        $mask = str_repeat($maskChar, $maskLen);
        if ($keepEnd) {
            return substr($str, 0, $start) . $mask . substr($str, $start + $maskLen, 1);
        }
        return substr($str, 0, $start) . $mask . substr($str, $start + $maskLen);
    }
}
// 使用示例
class User
{
    #[MaskAttribute(type: 'name')]
    public string $name;
    #[MaskAttribute(type: 'mobile')]
    public string $phone;
    #[MaskAttribute(type: 'email')]
    public string $email;
    #[MaskAttribute(start: 4, length: 4, keepEnd: true)]
    public string $address;
    public function __construct(string $name, string $phone, string $email, string $address)
    {
        $this->name = $name;
        $this->phone = $phone;
        $this->email = $email;
        $this->address = $address;
    }
}
// 使用
$user = new User('张三', '13812345678', 'test@example.com', '北京市朝阳区某某街道');
MaskHandler::handle($user);
print_r($user);

中间件脱敏方案(Laravel示例)

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class DataMaskingMiddleware
{
    protected $maskingRules = [
        'users' => [
            'phone' => 'mobile',
            'email' => 'email',
            'id_card' => 'id_card',
            'name' => 'name'
        ],
        'orders' => [
            'address' => 'address',
            'contact_phone' => 'mobile'
        ]
    ];
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        if ($response instanceof \Illuminate\Http\JsonResponse) {
            $data = json_decode($response->getContent(), true);
            if ($this->shouldMask($request)) {
                $maskedData = $this->applyMasking($data);
                $response->setData($maskedData);
            }
        }
        return $response;
    }
    protected function shouldMask(Request $request): bool
    {
        // 如果不是浏览器请求或者需要脱敏的环境
        if ($request->is('api/*')) {
            return true;
        }
        return false;
    }
    protected function applyMasking(array $data): array
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = $this->applyMasking($value);
            } else {
                // 检查是否有对应的脱敏规则
                $model = $this->getModelName($data);
                if (isset($this->maskingRules[$model][$key])) {
                    $data[$key] = DataMasking::mask($value, $this->maskingRules[$model][$key]);
                }
            }
        }
        return $data;
    }
    protected function getModelName(array $data): string
    {
        // 根据数据特征判断模型名称
        if (isset($data['user_id'])) {
            return 'users';
        }
        if (isset($data['order_id'])) {
            return 'orders';
        }
        return 'default';
    }
}

数据库查询时脱敏(SQL方案)

<?php
class SqlMaskingBuilder
{
    /**
     * 生成SQL脱敏表达式
     */
    public static function buildMaskExpression(string $column, string $type): string
    {
        switch ($type) {
            case 'mobile':
                return "CONCAT(LEFT($column, 3), '****', RIGHT($column, 4))";
            case 'id_card':
                return "CONCAT(LEFT($column, 4), '**********', RIGHT($column, 4))";
            case 'name':
                return "CONCAT(LEFT($column, 1), REPEAT('*', CHAR_LENGTH($column) - 1))";
            case 'email':
                return "CONCAT(LEFT($column, 3), '***', SUBSTRING_INDEX($column, '@', -1))";
            default:
                return $column;
        }
    }
    /**
     * 生成脱敏SELECT语句
     */
    public static function maskedSelect(string $table, array $columns, array $maskingRules): string
    {
        $selectColumns = [];
        foreach ($columns as $column) {
            $selectColumns[] = self::buildMaskExpression($column, $maskingRules[$column] ?? 'default');
        }
        return "SELECT " . implode(', ', $selectColumns) . " FROM $table";
    }
}
// 使用示例
$sql = SqlMaskingBuilder::maskedSelect(
    'users',
    ['id', 'name', 'phone', 'email'],
    ['name' => 'name', 'phone' => 'mobile', 'email' => 'email']
);

自定义脱敏配置(配置文件方式)

<?php
// config/masking.php
return [
    'users' => [
        'name' => [
            'type' => 'name',
            'enabled' => true
        ],
        'phone' => [
            'type' => 'mobile',
            'enabled' => true
        ],
        'email' => [
            'type' => 'email',
            'enabled' => true,
            'case_sensitive' => false
        ],
        'address' => [
            'type' => 'custom',
            'start' => 6,
            'maskChar' => '*',
            'keepEnd' => false
        ]
    ],
    'settings' => [
        'default_mask_char' => '*',
        'mask_percentage' => 70
    ]
];
class MaskingService
{
    private $config;
    public function __construct(array $config)
    {
        $this->config = $config;
    }
    public function maskField(string $model, string $field, $value)
    {
        if (!isset($this->config[$model][$field])) {
            return $value;
        }
        $rule = $this->config[$model][$field];
        if (isset($rule['enabled']) && !$rule['enabled']) {
            return $value;
        }
        if ($rule['type'] === 'custom') {
            return $this->customMask($value, $rule);
        }
        return DataMasking::mask($value, $rule['type']);
    }
    private function customMask($value, array $rule): string
    {
        $start = $rule['start'] ?? 0;
        $maskChar = $rule['maskChar'] ?? '*';
        $keepEnd = $rule['keepEnd'] ?? true;
        // 自定义脱敏逻辑
        $str = (string)$value;
        $len = strlen($str);
        if ($start >= $len) {
            return $str;
        }
        // 计算脱敏长度
        $maskLen = ceil($len * ($this->config['settings']['mask_percentage'] ?? 70) / 100);
        $maskLen = min($maskLen, $len - $start);
        $mask = str_repeat($maskChar, $maskLen);
        if ($keepEnd) {
            return substr($str, 0, $start) . $mask . substr($str, $start + $maskLen);
        }
        return substr($str, 0, $start) . $mask;
    }
}
// 使用示例
$maskingService = new MaskingService(require 'config/masking.php');
echo $maskingService->maskField('users', 'name', '张三丰');

完整的脱敏工具类(包含所有常用类型)

<?php
class ComprehensiveMasker
{
    // 定义脱敏类型常量
    const TYPE_MOBILE = 'mobile';
    const TYPE_PHONE = 'phone';
    const TYPE_EMAIL = 'email';
    const TYPE_ID_CARD = 'id_card';
    const TYPE_BANK_CARD = 'bank_card';
    const TYPE_NAME = 'name';
    const TYPE_ADDRESS = 'address';
    const TYPE_IP = 'ip';
    const TYPE_PASSWORD = 'password';
    const TYPE_TAX_NO = 'tax_no';
    const TYPE_LICENSE = 'license';
    // 掩码字符映射
    private static $maskChar = '*';
    /**
     * 统一脱敏入口
     */
    public static function mask($value, string $type): string
    {
        if ($value === null || $value === '') {
            return $value ?? '';
        }
        switch ($type) {
            case self::TYPE_MOBILE:
                return self::maskMobile($value);
            case self::TYPE_PHONE:
                return self::maskPhone($value);
            case self::TYPE_EMAIL:
                return self::maskEmail($value);
            case self::TYPE_ID_CARD:
                return self::maskIdCard($value);
            case self::TYPE_BANK_CARD:
                return self::maskBankCard($value);
            case self::TYPE_NAME:
                return self::maskName($value);
            case self::TYPE_ADDRESS:
                return self::maskAddress($value);
            case self::TYPE_IP:
                return self::maskIp($value);
            case self::TYPE_PASSWORD:
                return self::maskPassword($value);
            case self::TYPE_TAX_NO:
                return self::maskTaxNo($value);
            case self::TYPE_LICENSE:
                return self::maskLicense($value);
            default:
                return self::maskDefault($value);
        }
    }
    private static function maskMobile($mobile): string
    {
        return preg_replace('/^(\d{3})\d{4}(\d{4})$/', '$1****$2', $mobile);
    }
    private static function maskPhone($phone): string
    {
        // 支持座机号码脱敏
        return preg_replace('/^(\d{3,4})(\d{4})(\d{4})$/', '$1****$3', $phone);
    }
    private static function maskEmail($email): string
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $email;
        }
        list($username, $domain) = explode('@', $email);
        $maskedUsername = substr($username, 0, 3) . str_repeat(self::$maskChar, strlen($username) - 3);
        return $maskedUsername . '@' . $domain;
    }
    private static function maskIdCard($idCard): string
    {
        if (strlen($idCard) === 18) {
            return substr($idCard, 0, 4) . str_repeat(self::$maskChar, 10) . substr($idCard, 14);
        }
        if (strlen($idCard) === 15) {
            return substr($idCard, 0, 4) . str_repeat(self::$maskChar, 7) . substr($idCard, 11);
        }
        return $idCard;
    }
    private static function maskBankCard($cardNo): string
    {
        return substr($cardNo, 0, 4) . str_repeat(self::$maskChar, 8) . substr($cardNo, -4);
    }
    private static function maskName($name): string
    {
        if (mb_strlen($name) <= 1) {
            return $name;
        }
        // 不同类型姓名处理
        if (mb_strlen($name) === 2) {
            // 两个字:保留姓氏
            return mb_substr($name, 0, 1) . self::$maskChar;
        }
        return mb_substr($name, 0, 1) . str_repeat(self::$maskChar, mb_strlen($name) - 2) . mb_substr($name, -1);
    }
    private static function maskAddress($address): string
    {
        $length = mb_strlen($address);
        if ($length <= 4) {
            return $address;
        }
        // 保留前4个字符,脱敏中间部分,保留后2个字符
        $maskCount = $length - 6;
        return mb_substr($address, 0, 4) . 
               str_repeat(self::$maskChar, max(0, $maskCount)) . 
               mb_substr($address, -2);
    }
    private static function maskIp($ip): string
    {
        $parts = explode('.', $ip);
        if (count($parts) != 4) {
            return $ip;
        }
        return $parts[0] . '.' . self::$maskChar . '.' . self::$maskChar . '.' . $parts[3];
    }
    private static function maskPassword($password): string
    {
        return str_repeat(self::$maskChar, strlen($password));
    }
    private static function maskTaxNo($taxNo): string
    {
        if (strlen($taxNo) === 15 || strlen($taxNo) === 18) {
            return substr($taxNo, 0, 2) . str_repeat(self::$maskChar, strlen($taxNo) - 6) . substr($taxNo, -4);
        }
        return $taxNo;
    }
    private static function maskLicense($license): string
    {
        // 营业执照号脱敏
        if (strlen($license) === 18) {
            return '913***********' . substr($license, -4);
        }
        return $license;
    }
    private static function maskDefault($value): string
    {
        if (strlen($value) <= 3) {
            return self::$maskChar;
        }
        return substr($value, 0, 3) . str_repeat(self::$maskChar, strlen($value) - 3);
    }
}
// 批量脱敏函数
function maskArray(array $data, array $rules): array
{
    foreach ($rules as $field => $type) {
        if (isset($data[$field])) {
            $data[$field] = ComprehensiveMasker::mask($data[$field], $type);
        }
    }
    return $data;
}
// 使用示例
$userData = [
    'name' => '张三丰',
    'phone' => '13812345678',
    'email' => 'test@example.com',
    'id_card' => '110101199001011234',
    'address' => '北京市朝阳区某某街道123号',
    'ip' => '192.168.1.1'
];
$rules = [
    'name' => ComprehensiveMasker::TYPE_NAME,
    'phone' => ComprehensiveMasker::TYPE_MOBILE,
    'email' => ComprehensiveMasker::TYPE_EMAIL,
    'id_card' => ComprehensiveMasker::TYPE_ID_CARD,
    'address' => ComprehensiveMasker::TYPE_ADDRESS,
    'ip' => ComprehensiveMasker::TYPE_IP
];
$maskedData = maskArray($userData, $rules);
print_r($maskedData);

总结与建议

脱敏方案选择建议:

  1. 性能要求高:使用SQL层面的脱敏
  2. 可维护性要求高:使用注解或配置驱动的方式
  3. 安全要求高:在后端强制脱敏,前端不接收敏感数据
  4. 数据量大:采用分批脱敏,避免一次性处理

最佳实践:

  • ✅ 在生产环境默认启用脱敏
  • ✅ 提供脱敏开关,用于测试环境
  • ✅ 脱敏规则集中管理
  • ✅ 敏感数据加密存储
  • ✅ 日志中也进行脱敏

完整示例下载地址:

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