敏感数据保护贯穿数据的完整生命周期,其中最核心的两个环节是 存储时的加密 和 展示时的脱敏。前者保证即使数据库泄露也无法还原明文,后者确保在日志、接口响应、界面等展示渠道中敏感字段不被完整暴露。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 全链路保护的最佳实践
- 最小化明文接触:敏感数据进入系统后,尽快加密存储,仅在必要时解密,且解密范围尽可能小(如在 Value Object 中,用完立即丢弃)。
- 分层防御:
- 网络层:TLS 保证传输加密。
- 应用层:字段加密 + 脱敏序列化。
- 日志层:全局脱敏或禁止打印敏感参数。
- 存储层:数据库加密(应用加密或 TDE)。
- 密钥安全:使用专用的密钥管理服务(HashiCorp Vault、AWS KMS、阿里云 KMS),避免将密钥与代码一同存储或传输。
- 审计与监控:记录谁在何时访问了明文数据,对异常解密操作告警。
- 合规性对齐:涉及个人信息保护法、GDPR 等法规时,需确保脱敏和加密策略满足要求(如去标识化、匿名化)。
通过合理的加密存储与分层脱敏设计,可以显著降低敏感数据泄露后的影响,使系统在满足业务可用性的同时具备坚实的数据安全防线。