本文目录导读:

- 核心思路:在写入数据库前拦截处理
- 案例一:基于 MyBatis 的 TypeHandler(最推荐)
- 案例二:基于 Jackson 的序列化脱敏(适用于 JSON 存储)
- 案例三:基于 AOP 或拦截器(通用性强)
- 案例四:数据库内部脱敏(MySQL函数层)
- 安全性对比与选择建议
- 重要警告:密码存储
在Java中实现数据存储脱敏,通常是指在数据写入数据库(或日志、文件)之前,对敏感字段(如手机号、身份证号、银行卡号、密码等)进行转换或加密,以达到保护敏感信息的目的。
根据业务场景和安全性要求的不同,存储脱敏的实现方式主要有以下几种,以下是详细的案例和代码实现。
核心思路:在写入数据库前拦截处理
最标准的做法是在DAO层(如MyBatis、JPA的实体类或拦截器)或ORM框架的拦截器中完成脱敏,这样业务代码无感知,实现通用性。
基于 MyBatis 的 TypeHandler(最推荐)
这是最优雅、侵入性最低的方式,通过自定义 TypeHandler,在数据 setParameter(写入DB)时自动脱敏,在 getResult(读取DB)时还原。
自定义脱敏类型
// 定义一个标记接口,标识哪些字段需要脱敏
public interface SensitiveData {
String getSensitiveValue();
void setSensitiveValue(String value);
}
// 手机号实体
public class PhoneNumber implements SensitiveData {
private String value;
public PhoneNumber(String value) {
this.value = value;
}
@Override
public String getSensitiveValue() {
return maskPhone(value); // 脱敏逻辑:138****1234
}
@Override
public void setSensitiveValue(String value) {
this.value = value;
}
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
@Override
public String toString() {
return value;
}
}
自定义 TypeHandler
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;
public class SensitiveTypeHandler extends BaseTypeHandler<SensitiveData> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
SensitiveData parameter, JdbcType jdbcType) throws SQLException {
// 写入时:调用脱敏方法
ps.setString(i, parameter.getSensitiveValue());
}
@Override
public SensitiveData getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
// 读取时:直接返回原始值(假设数据库存的就是脱敏后的,按需处理)
return new PhoneNumber(value);
}
@Override
public SensitiveData getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return new PhoneNumber(value);
}
@Override
public SensitiveData getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return new PhoneNumber(value);
}
}
实体类中使用
public class User {
private Long id;
private String name;
// 使用TypeHandler
private PhoneNumber phone;
// getters/setters...
}
MyBatis 配置(XML或注解)
<!-- 在resultMap或插入语句中指定handler -->
<resultMap id="userMap" type="User">
<result column="phone" property="phone" typeHandler="com.example.handler.SensitiveTypeHandler"/>
</resultMap>
<insert id="insertUser" parameterType="User">
INSERT INTO user (name, phone) VALUES (#{name}, #{phone, typeHandler=com.example.handler.SensitiveTypeHandler})
</insert>
优点:与业务完全解耦,所有写入操作自动脱敏。 缺点:需要为每个敏感类型写一个TypeHandler。
基于 Jackson 的序列化脱敏(适用于 JSON 存储)
如果数据库存储的是JSON字段(如MySQL的json类型),或数据传输时只输出脱敏数据,可以在Spring Boot中自定义Jackson序列化器。
自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerializer.class)
public @interface Sensitive {
String strategy() default "DEFAULT"; // 策略:PHONE, ID_CARD, BANK_CARD
}
序列化器实现
public class SensitiveSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 这里简单处理,实际可通过自定义注解获取策略
String masked = maskPhone(value);
gen.writeString(masked);
}
private String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
实体类使用
public class UserJson {
private Long id;
@Sensitive(strategy = "PHONE")
private String phone;
// getters/setters
}
效果:当 UserJson 被 ObjectMapper 序列化为JSON时(比如返回给前端),phone 字段自动脱敏,但注意:如果直接写入数据库,这个方法仍然写入原始值。解决办法:在 @PreUpdate/@PrePersist 中手动调用脱敏方法,或结合AOP。
基于 AOP 或拦截器(通用性强)
通过Spring AOP或自定义注解,在DAO方法执行前拦截并修改参数。
定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveDataMask {
}
AOP切面
@Aspect
@Component
public class SensitiveDataAspect {
@Around("@annotation(com.example.annotation.SensitiveDataMask)")
public Object maskSensitiveData(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof List) {
((List<?>) arg).forEach(this::maskObject);
} else {
maskObject(arg);
}
}
return joinPoint.proceed();
}
private void maskObject(Object obj) {
// 使用反射遍历字段,找到有@Sensitive注解的字段进行脱敏
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Sensitive.class)) {
try {
field.setAccessible(true);
String original = (String) field.get(obj);
if (original != null) {
field.set(obj, mask(original, field.getAnnotation(Sensitive.class).strategy()));
}
} catch (IllegalAccessException e) {
// log error
}
}
}
}
private String mask(String value, String strategy) {
// 根据策略脱敏
return value; // 实际实现
}
}
在Service方法上使用
@Service
public class UserService {
@SensitiveDataMask
public void saveUser(User user) {
userMapper.insert(user); // 此时user对象的phone已被脱敏
}
}
数据库内部脱敏(MySQL函数层)
如果项目底层无法改代码,可以在数据库层使用触发器或自定义函数脱敏。
-- MySQL 触发器示例:插入前脱敏
DELIMITER $$
CREATE TRIGGER mask_phone_before_insert
BEFORE INSERT ON user FOR EACH ROW
BEGIN
SET NEW.phone = CONCAT(SUBSTRING(NEW.phone, 1, 3), '****', SUBSTRING(NEW.phone, 8, 4));
END$$
DELIMITER ;
缺点:难以维护,不推荐用于新项目。
安全性对比与选择建议
| 方式 | 安全性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| MyBatis TypeHandler | 高 | 低 | 低 | 新项目,ORM统一 |
| Jackson序列化 | 中(需配合预处理) | 中 | 低 | JSON存储或接口输出 |
| AOP拦截器 | 高 | 中 | 中 | 已有系统改造 |
| 数据库触发器 | 高 | 高 | 高 | 无法修改代码的遗留系统 |
重要警告:密码存储
对于密码,永远不要使用可逆脱敏,正确的做法是:
- 使用加盐的哈希算法(如bcrypt、argon2)。
- 存储哈希值(
$2a$10$...),原始密码不可恢复。 - 验证时比较哈希值。
讨论的脱敏适用于手机号、身份证、邮箱、银行卡号等需要部分可读或可恢复的场景。
- 存储脱敏的核心是 “写时转换,读时还原”。
- 推荐首选 MyBatis TypeHandler 或 JPA AttributeConverter(原理类似)。
- 对于微服务或新项目,可以封装一个通用的敏感数据脱敏注解框架。
- 务必区分“脱敏”(部分遮掩或可逆加密)与“哈希加密”(不可逆),密码是后者。
如果需要具体场景的完整代码(如身份证、银行卡的脱敏规则),可以进一步提供。