Java案例如何实现存储脱敏?

wen java案例 75

本文目录导读:

Java案例如何实现存储脱敏?

  1. 核心思路:在写入数据库前拦截处理
  2. 案例一:基于 MyBatis 的 TypeHandler(最推荐)
  3. 案例二:基于 Jackson 的序列化脱敏(适用于 JSON 存储)
  4. 案例三:基于 AOP 或拦截器(通用性强)
  5. 案例四:数据库内部脱敏(MySQL函数层)
  6. 安全性对比与选择建议
  7. 重要警告:密码存储

在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
}

效果:当 UserJsonObjectMapper 序列化为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拦截器 已有系统改造
数据库触发器 无法修改代码的遗留系统

重要警告:密码存储

对于密码,永远不要使用可逆脱敏,正确的做法是:

  1. 使用加盐的哈希算法(如bcrypt、argon2)。
  2. 存储哈希值($2a$10$...),原始密码不可恢复。
  3. 验证时比较哈希值。

讨论的脱敏适用于手机号、身份证、邮箱、银行卡号等需要部分可读或可恢复的场景。

  • 存储脱敏的核心是 “写时转换,读时还原”
  • 推荐首选 MyBatis TypeHandlerJPA AttributeConverter(原理类似)。
  • 对于微服务或新项目,可以封装一个通用的敏感数据脱敏注解框架。
  • 务必区分“脱敏”(部分遮掩或可逆加密)与“哈希加密”(不可逆),密码是后者。

如果需要具体场景的完整代码(如身份证、银行卡的脱敏规则),可以进一步提供。

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