人人都会AI编程

23.3 敏感数据脱敏与加密存储

更新时间:2026-07-11

敏感数据保护贯穿数据的完整生命周期,其中最核心的两个环节是 存储时的加密展示时的脱敏。前者保证即使数据库泄露也无法还原明文,后者确保在日志、接口响应、界面等展示渠道中敏感字段不被完整暴露。Spring 生态中已有成熟方案可实现这两类需求。

23.3.1 数据库存储加密

对数据库中存储的敏感字段(如手机号、身份证号、银行卡号)进行加密,是纵深防御的关键一环。常见做法是在应用层完成加解密,对业务代码尽量透明。

1. 方案选择

  • 应用层加解密:灵活性最高,可结合密钥管理服务(KMS)动态获取密钥。推荐使用 AES-256 对称加密。
  • 数据库透明加密(TDE):由数据库引擎在存储层完成加解密,对应用完全透明。适合整体文件级加密,但无法做到字段级别的细粒度控制。
  • 文件系统/磁盘加密:仅防范物理盗窃,对应用攻击无效。

实践中,字段级加密多采用应用层方案。

2. 实现:基于 JPA 的属性转换器

JPA 提供了 AttributeConverter 接口,可以在实体属性与数据库列之间自动转换。对于使用 JPA 的项目,这是最轻量的字段加密方式。

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.Base64;

@Converter
public class AesEncryptor implements AttributeConverter<String, String> {

    private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
    // 密钥应从外部配置获取,此处仅为示例
    private static final byte[] KEY = "my-32-byte-secret-key-123456".getBytes();

    @Override
    public String convertToDatabaseColumn(String attribute) {
        if (attribute == null) return null;
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(KEY, "AES");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(attribute.getBytes("UTF-8"));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败", e);
        }
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec keySpec = new SecretKeySpec(KEY, "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(dbData));
            return new String(decrypted, "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException("解密失败", e);
        }
    }
}

在实体字段上应用该转换器即可:

@Entity
public class User {
    @Id
    private Long id;

    @Convert(converter = AesEncryptor.class)
    private String phoneNumber;   // 数据库中存储密文,Java 对象中为明文

    // ...
}

注意:转换器在查询时仍以密文匹配,无法对加密字段进行模糊查询。如需支持 LIKE 查询,需额外建立索引或使用可搜索加密方案(如盲索引),复杂度较高。

3. 密钥管理

密钥绝不能硬编码在代码中。生产环境应集成 KMS 或至少从环境变量/配置中心获取:

@Value("${app.encryption.key}")
private String encryptionKey;

结合 Spring Cloud Config 或 Vault 可进一步实现密钥的动态刷新与安全分发。

23.3.2 日志脱敏与接口响应脱敏

即使数据库已加密,服务运行过程中产生的日志、返回给前端的 JSON 仍可能将敏感信息完整输出。脱敏的核心思路是在数据离开应用边界前,对敏感内容进行遮蔽处理。

1. 日志脱敏:基于 Logback 的 MessageConverter

通过自定义 MessageConverter,可以在日志输出前对消息体进行正则替换或基于上下文替换。

import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

public class SensitiveDataConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event) {
        String message = super.convert(event);
        // 正则匹配手机号、身份证号等并替换
        message = message.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); // 手机号
        message = message.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2"); // 身份证
        return message;
    }
}

logback-spring.xml 中注册:

<conversionRule conversionWord="msg" 
    converterClass="com.example.config.SensitiveDataConverter" />

更精细的控制可结合 MDC 和 AOP,在日志时自动为敏感参数打码。

2. 接口响应脱敏:Jackson 自定义序列化器

对外接口返回 JSON 时,常需对特定字段(手机号、身份证、姓名等)进行局部遮蔽。可通过自定义 Jackson 序列化器实现。

定义脱敏注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface Sensitive {
    SensitiveType value() default SensitiveType.DEFAULT;
}

public enum SensitiveType {
    NAME,        // 姓名,保留首字,其他打*
    PHONE,       // 手机号,保留前3后4
    ID_CARD      // 身份证,保留前4后4
}

实现序列化器:

public class SensitiveSerialize extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        Sensitive annotation = (Sensitive) gen.getOutputContext().getCurrentValue()
                .getClass().getDeclaredField(gen.getOutputContext().getCurrentName())
                .getAnnotation(Sensitive.class);
        SensitiveType type = annotation != null ? annotation.value() : SensitiveType.DEFAULT;
        switch (type) {
            case PHONE:
                gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
                break;
            case NAME:
                gen.writeString(value.charAt(0) + "*".repeat(value.length() - 1));
                break;
            default:
                gen.writeString(value);
        }
    }
}

字段使用:

public class UserInfo {
    @Sensitive(SensitiveType.NAME)
    private String realName;

    @Sensitive(SensitiveType.PHONE)
    private String phone;

    // getter/setter...
}

Spring Boot 默认使用 Jackson 序列化,此类注解即自动生效。

3. 直接封装脱敏工具类

对于非 JSON 场景(如导出报表、页面渲染),可提供一个静态工具方法:

public class MaskUtils {
    public static String maskPhone(String phone) {
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    public static String maskIdCard(String id) {
        return id.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
    }
}

在需要的地方显式调用。

23.3.3 全链路保护的最佳实践

  1. 最小化明文接触:敏感数据进入系统后,尽快加密存储,仅在必要时解密,且解密范围尽可能小(如在 Value Object 中,用完立即丢弃)。
  2. 分层防御
  • 网络层:TLS 保证传输加密。
  • 应用层:字段加密 + 脱敏序列化。
  • 日志层:全局脱敏或禁止打印敏感参数。
  • 存储层:数据库加密(应用加密或 TDE)。
  1. 密钥安全:使用专用的密钥管理服务(HashiCorp Vault、AWS KMS、阿里云 KMS),避免将密钥与代码一同存储或传输。
  2. 审计与监控:记录谁在何时访问了明文数据,对异常解密操作告警。
  3. 合规性对齐:涉及个人信息保护法、GDPR 等法规时,需确保脱敏和加密策略满足要求(如去标识化、匿名化)。

通过合理的加密存储与分层脱敏设计,可以显著降低敏感数据泄露后的影响,使系统在满足业务可用性的同时具备坚实的数据安全防线。