ThinkPHP项目敏感数据脱敏展示

wen PHP项目 4

本文目录导读:

ThinkPHP项目敏感数据脱敏展示

  1. 核心思路与工具类封装
  2. 模型层自动脱敏(推荐方案)
  3. 控制器/服务层手动脱敏(灵活方案)
  4. 日志与调试的脱敏(防止敏感信息泄露)
  5. 前端脱敏(补充方案)
  6. 总结建议

在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)实现自动脱敏,这样,只要查询该模型的数据,敏感字段就会自动处理,无需在控制器中手动调用。

使用步骤:

  1. 在模型类中引入 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');

前端脱敏(补充方案)

如果后端接口需要给前端返回原始数据(用于编辑回显),但前端展示时需要脱敏,可以在模型中使用 hiddenvisible 配合 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接口返回数据 模型获取器 (方案二) 或 手动调用工具类 (方案三)
后端日志记录 调用工具类 (方案四)
前端编辑回显 + 列表展示 双字段返回 (方案五)
支持多种复杂脱敏规则 封装通用工具类 (方案一)

核心建议: 优先使用模型层的获取器机制,它的侵入性最低,且能全局生效,避免在控制器中漏写脱敏逻辑,如果你的项目对原始数据的完整性要求极高(例如需要在后端进行计算),那么建议使用“双字段返回”方案,将原始字段和脱敏字段同时返回,前端按需使用。

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