数据脱敏从存储加密到展示脱敏完整链路

作者:忆笙智云官方 | 发布时间:2026-05-04 13:30 | 更新时间:2026-07-04 13:30

数据脱敏从存储加密到展示脱敏完整链路

引言

在企业级应用中,敏感数据保护是合规和安全的核心要求。《个人信息保护法》《数据安全法》等法规明确要求对手机号、身份证号、银行卡号等个人信息进行脱敏处理。然而,很多项目中的脱敏方案存在以下问题:

本文分享一套从存储加密到展示脱敏的完整链路方案,基于 SM4 国密算法加密存储、自定义注解控制脱敏/解密行为、权限感知的原始值查看机制,实现敏感数据的全生命周期保护。

核心内容

一、整体架构

数据脱敏完整链路覆盖数据的写入、存储、读取、展示四个阶段:

flowchart TB
    A[前端提交敏感数据] --> B[@SensitiveEncode 注解拦截]
    B --> C[SM4 加密处理]
    C --> D[密文写入数据库]

    D --> E[数据库存储密文]
    E --> F[查询返回密文数据]
    F --> G{@SensitiveDecode 注解?}
    G -->|是| H[SM4 解密为明文]
    G -->|否| I[保留密文]

    H --> J{@SensitiveField 注解?}
    I --> J
    J -->|用户有脱敏权限| K[返回原始明文值]
    J -->|用户无脱敏权限| L[按类型脱敏展示]

    L --> M[手机号: 138****1234]
    L --> N[身份证: 310***********1234]
    L --> O[银行卡: **** **** **** 5678]
    L --> P[姓名: 张*]
    L --> Q[邮箱: a***@example.com]
    L --> R[地址: 上海市***]

二、SM4 加密存储

SM4 是我国自主研发的分组密码算法,密钥长度 128 位,安全性与 AES-128 相当,且符合国密合规要求。

2.1 SM4 工具类

/**
 * SM4 国密加密解密工具类
 * 支持 ECB 和 CBC 两种模式
 */
public class Sm4Util {

    private static final String ALGORITHM = "SM4";
    private static final String ECB_MODE = "SM4/ECB/PKCS5Padding";
    private static final String CBC_MODE = "SM4/CBC/PKCS5Padding";

    /**
     * SM4 ECB 模式加密
     * @param data 明文数据
     * @param key  密钥(16字节)
     * @return Base64 编码的密文
     */
    public static String encryptEcb(String data, String key) {
        try {
            Cipher cipher = Cipher.getInstance(ECB_MODE, BouncyCastleProvider.PROVIDER_NAME);
            Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("SM4加密失败", e);
        }
    }

    /**
     * SM4 ECB 模式解密
     * @param data Base64 编码的密文
     * @param key  密钥(16字节)
     * @return 明文数据
     */
    public static String decryptEcb(String data, String key) {
        try {
            Cipher cipher = Cipher.getInstance(ECB_MODE, BouncyCastleProvider.PROVIDER_NAME);
            Key secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(data));
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("SM4解密失败", e);
        }
    }
}

2.2 密钥管理建议

密钥不应硬编码在代码中,推荐以下管理方式:

flowchart LR
    A[应用启动] --> B{密钥来源}
    B -->|生产环境| C[密钥管理服务 KMS]
    B -->|开发环境| D[配置中心加密存储]
    C --> E[运行时获取密钥]
    D --> E
    E --> F[缓存到内存]
    F --> G[定时轮换密钥]

三、@SensitiveField 脱敏类型

通过自定义注解标记字段的脱敏类型,在序列化阶段自动进行脱敏处理。

3.1 脱敏类型枚举

/**
 * 脱敏类型枚举
 * 定义常见敏感数据的脱敏策略
 */
public enum SensitiveType {

    /** 手机号:保留前3后4,中间用*代替 */
    PHONE,

    /** 身份证号:保留前3后4,中间用*代替 */
    ID_CARD,

    /** 银行卡号:保留后4位,其余用*代替 */
    BANK_CARD,

    /** 姓名:保留姓,其余用*代替 */
    NAME,

    /** 地址:保留省市区,其余用*代替 */
    ADDRESS,

    /** 邮箱:保留首字符和@后域名,中间用*代替 */
    EMAIL,

    /** 自定义脱敏策略 */
    CUSTOM
}

3.2 @SensitiveField 注解

/**
 * 敏感字段脱敏注解
 * 标注在实体类字段上,指定脱敏类型和策略
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveFieldSerializer.class)
public @interface SensitiveField {

    /** 脱敏类型 */
    SensitiveType value();

    /** 左侧保留字符数(仅CUSTOM类型生效) */
    int leftKeep() default 0;

    /** 右侧保留字符数(仅CUSTOM类型生效) */
    int rightKeep() default 0;

    /** 替换字符 */
    char maskChar() default '*';
}

3.3 脱敏序列化器

/**
 * 脱敏字段序列化器
 * 在 JSON 序列化时根据脱敏类型自动处理
 */
public class SensitiveFieldSerializer extends JsonSerializer<String>
        implements ContextualSerializer {

    private SensitiveType type;
    private int leftKeep;
    private int rightKeep;
    private char maskChar;

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
            throws IOException {
        if (value == null || value.isEmpty()) {
            gen.writeString(value);
            return;
        }

        // 检查当前用户是否有查看原始值的权限
        if (hasSensitivePermission()) {
            gen.writeString(value);
            return;
        }

        // 根据脱敏类型处理
        String masked = switch (type) {
            case PHONE -> maskValue(value, 3, 4);
            case ID_CARD -> maskValue(value, 3, 4);
            case BANK_CARD -> maskValue(value, 0, 4);
            case NAME -> maskName(value);
            case ADDRESS -> maskAddress(value);
            case EMAIL -> maskEmail(value);
            case CUSTOM -> maskValue(value, leftKeep, rightKeep);
        };
        gen.writeString(masked);
    }

    /**
     * 通用脱敏方法
     * @param value 原始值
     * @param left  左侧保留位数
     * @param right 右侧保留位数
     * @return 脱敏后的值
     */
    private String maskValue(String value, int left, int right) {
        if (value.length() <= left + right) {
            return value;
        }
        int maskLen = value.length() - left - right;
        String mask = String.valueOf(maskChar).repeat(maskLen);
        return value.substring(0, left) + mask + value.substring(value.length() - right);
    }

    /**
     * 检查当前用户是否拥有脱敏查看权限
     */
    private boolean hasSensitivePermission() {
        // 通过安全框架获取当前用户的权限信息
        // 如 Sa-Token: StpUtil.hasPermission("sensitive:view")
        return SecurityUtil.hasPermission("sensitive:view");
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        SensitiveField annotation = property.getAnnotation(SensitiveField.class);
        if (annotation != null) {
            SensitiveFieldSerializer serializer = new SensitiveFieldSerializer();
            serializer.type = annotation.value();
            serializer.leftKeep = annotation.leftKeep();
            serializer.rightKeep = annotation.rightKeep();
            serializer.maskChar = annotation.maskChar();
            return serializer;
        }
        return this;
    }
}

3.4 实体类使用示例

public class UserDTO {

    @SensitiveField(SensitiveType.NAME)
    private String realName;

    @SensitiveField(SensitiveType.PHONE)
    private String phone;

    @SensitiveField(SensitiveType.ID_CARD)
    private String idCard;

    @SensitiveField(SensitiveType.BANK_CARD)
    private String bankCard;

    @SensitiveField(SensitiveType.EMAIL)
    private String email;

    @SensitiveField(SensitiveType.ADDRESS)
    private String address;

    // getter/setter 省略
}

脱敏效果示例:

字段 原始值 脱敏后
姓名 张三丰 张**
手机号 13812341234 138****1234
身份证 310101199001011234 310***********1234
银行卡 6222021234565678 ************5678
邮箱 admin@example.com a***@example.com
地址 上海市浦东新区张江镇 上海市浦东新区***

四、@SensitiveEncode / @SensitiveDecode 注解

存储加密和查询解密通过 AOP 注解控制,与业务代码解耦。

4.1 注解定义

/**
 * 敏感数据加密注解
 * 标注在方法上,方法参数中的敏感字段在入库前自动加密
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveEncode {
    /** 需要加密的字段名列表,为空则自动检测 @SensitiveField 标注的字段 */
    String[] fields() default {};
}

/**
 * 敏感数据解密注解
 * 标注在方法上,返回结果中的敏感字段自动解密
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveDecode {
    /** 需要解密的字段名列表,为空则自动检测 @SensitiveField 标注的字段 */
    String[] fields() default {};
}

4.2 AOP 切面实现

/**
 * 敏感数据加解密切面
 * 拦截标注了 @SensitiveEncode / @SensitiveDecode 的方法
 */
@Aspect
@Component
public class SensitiveAspect {

    @Autowired
    private SensitiveCryptoService cryptoService;

    /**
     * 加密切入点:方法执行前对参数中的敏感字段加密
     */
    @Before("@annotation(sensitiveEncode)")
    public void doEncode(JoinPoint joinPoint, SensitiveEncode sensitiveEncode) {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg == null) continue;
            // 通过反射找到标注了 @SensitiveField 的字段
            processFields(arg, (field, value) -> {
                if (value instanceof String strValue && !strValue.isEmpty()) {
                    // 判断是否已经是密文(密文通常以特定前缀标识)
                    if (!cryptoService.isEncrypted(strValue)) {
                        field.set(arg, cryptoService.encrypt(strValue));
                    }
                }
            });
        }
    }

    /**
     * 解密切入点:方法返回后对结果中的敏感字段解密
     */
    @AfterReturning(pointcut = "@annotation(sensitiveDecode)", returning = "result")
    public void doDecode(JoinPoint joinPoint, SensitiveDecode sensitiveDecode, Object result) {
        if (result == null) return;
        // 处理单个对象或列表
        if (result instanceof List<?> list) {
            list.forEach(item -> decryptObject(item));
        } else {
            decryptObject(result);
        }
    }

    private void decryptObject(Object obj) {
        processFields(obj, (field, value) -> {
            if (value instanceof String strValue && !strValue.isEmpty()) {
                if (cryptoService.isEncrypted(strValue)) {
                    field.set(obj, cryptoService.decrypt(strValue));
                }
            }
        });
    }

    @FunctionalInterface
    interface FieldProcessor {
        void process(Field field, Object value) throws IllegalAccessException;
    }

    private void processFields(Object obj, FieldProcessor processor) {
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(SensitiveField.class)) {
                field.setAccessible(true);
                try {
                    Object value = field.get(obj);
                    processor.process(field, value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("敏感字段处理失败", e);
                }
            }
        }
    }
}

4.3 业务层使用示例

@Service
public class UserService {

    /**
     * 新增用户 - 入库前自动加密敏感字段
     */
    @SensitiveEncode
    public void createUser(UserDTO user) {
        // user 中的 phone、idCard 等字段会自动被 SM4 加密
        userMapper.insert(user);
    }

    /**
     * 查询用户 - 返回时自动解密敏感字段
     * JSON 序列化时根据用户权限决定是否脱敏展示
     */
    @SensitiveDecode
    public UserDTO getUserById(Long id) {
        // 从数据库查出的密文会自动解密为明文
        // 返回给前端时,@SensitiveField 注解控制脱敏展示
        return userMapper.selectById(id);
    }

    /**
     * 更新用户 - 入库前自动加密
     */
    @SensitiveEncode
    public void updateUser(UserDTO user) {
        userMapper.updateById(user);
    }
}

五、权限感知的原始值查看

脱敏展示不是一刀切,有权限的用户(如客服、审计人员)需要查看原始值。

sequenceDiagram
    participant 前端
    participant API
    participant 序列化器
    participant 权限服务

    前端->>API: GET /api/user/1
    API->>API: @SensitiveDecode 解密密文
    API->>序列化器: 返回 UserDTO 对象
    序列化器->>权限服务: 查询当前用户权限
    alt 用户有 sensitive:view 权限
        权限服务-->>序列化器: 有权限
        序列化器-->>前端: 返回原始明文
    else 用户无权限
        权限服务-->>序列化器: 无权限
        序列化器-->>前端: 返回脱敏值
    end

5.1 前端权限控制

前端也可以通过权限标识控制是否展示脱敏切换按钮:

<template>
  <div>
    <span>{{ displayValue }}</span>
    <!-- 有脱敏查看权限时显示切换按钮 -->
    <el-button
      v-if="hasSensitivePermission"
      link
      @click="showOriginal = !showOriginal"
    >
      {{ showOriginal ? '隐藏' : '查看' }}
    </el-button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { hasPermission } from '@/utils/permission'

const props = defineProps({
  maskedValue: String,   // 脱敏值
  originalValue: String  // 原始值(仅授权用户接口返回)
})

const showOriginal = ref(false)
const hasSensitivePermission = hasPermission('sensitive:view')

const displayValue = computed(() => {
  if (hasSensitivePermission && showOriginal.value) {
    return props.originalValue
  }
  return props.maskedValue
})
</script>

六、完整数据流转示意

flowchart LR
    subgraph 写入链路
        A1[前端表单] --> A2[Controller]
        A2 --> A3["@SensitiveEncode"]
        A3 --> A4[SM4加密]
        A4 --> A5[密文存入DB]
    end

    subgraph 读取链路
        B1[查询DB密文] --> B2["@SensitiveDecode"]
        B2 --> B3[SM4解密]
        B3 --> B4[明文对象]
        B4 --> B5["@SensitiveField序列化"]
        B5 --> B6{有脱敏权限?}
        B6 -->|是| B7[返回明文]
        B6 -->|否| B8[返回脱敏值]
    end

结论与建议

核心要点

  1. 存储加密是底线:数据库中的敏感数据必须加密存储,SM4 国密算法是合规首选
  2. 展示脱敏是标配:API 返回数据时按类型脱敏,防止前端日志、浏览器缓存泄露
  3. 注解驱动零侵入:通过 @SensitiveField@SensitiveEncode@SensitiveDecode 三个注解,业务代码无需关心脱敏逻辑
  4. 权限感知灵活控制:不同角色看到不同粒度的数据,兼顾安全与业务需要

最佳实践建议

建议 说明
密钥与数据分离 加密密钥存储在 KMS 或配置中心,不要与数据库同库
密文标识 加密后的数据加前缀标识(如 ENC(),避免重复加密
日志脱敏 确保日志框架也对接脱敏机制,防止敏感数据打印到日志
定期审计 定期扫描数据库,检查是否存在未加密的敏感字段
密钥轮换 制定密钥轮换策略,建议每 90 天轮换一次

相关资源