跳至主要內容

数据脱敏设计方案

Leo数据安全数据安全约 1875 字大约 6 分钟

什么是数据脱敏

数据脱敏(Data Masking),又称为数据去标识化,是一种保护敏感数据的技术,通过替换、掩盖或混淆数据的某些部分,使其在暴露给未经授权的用户时无法识别或还原原始的敏感信息。这种技术广泛应用于开发、测试、分析和数据共享等场景中,以确保数据的安全性,同时保留数据在特定场景下的使用价值。

简单来说,数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。

在数据脱敏过程中,通常会采用不同的算法和技术,以根据不同的需求和场景对数据进行处理。例如,对于身份证号码,可以使用掩码算法(masking)将前几位数字保留,其他位用 "*" 代替。

常见的脱敏规则

  • 换(常用):将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(*)或其他字符。
  • 删除:将敏感数据中的部分内容随机删除。比如,将电话号码的随机 3 位数字进行删除。
  • 重排:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。
  • 加噪:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。
  • 加密(常用):使用加密算法将敏感数据转换为密文。例如,将银行卡号用 MD5 或 SHA-256 等哈希函数。

常见的几种脱敏方案

Hutool 工具类

Hutool 脱敏是通过 * 来代替敏感信息的,具体实现是在 StrUtil.hide 方法中。

import cn.hutool.core.util.DesensitizedUtil;
import org.junit.Test;
import org.springframework.boot.test.context.Spring BootTest;

/**
 *
 * @description: Hutool实现数据脱敏
 */
@Spring BootTest
public class HuToolDesensitizationTest {

    @Test
    public void testPhoneDesensitization(){
        String phone="13723231234";
        System.out.println(DesensitizedUtil.mobilePhone(phone)); //输出:137****1234
    }
    @Test
    public void testBankCardDesensitization(){
        String bankCard="6217000130008255666";
        System.out.println(DesensitizedUtil.bankCard(bankCard)); //输出:6217 **** **** *** 5666
    }

    @Test
    public void testIdCardNumDesensitization(){
        String idCardNum="411021199901102321";
        //只显示前4位和后2位
        System.out.println(DesensitizedUtil.idCardNum(idCardNum,4,2)); //输出:4110************21
    }
    @Test
    public void testPasswordDesensitization(){
        String password="www.jd.com_35711";
        System.out.println(DesensitizedUtil.password(password)); //输出:****************
    }
}

MyBatis-Flex

MyBatis-Flex 是一种基于 MyBatis 的开源 ORM 框架,提供了一些简单易用的方法来实现数据脱敏。其中最常用的方法是 MyBatisFlexMapper 中的 hidePassword 方法。

MyBatis-Flex 提供了 @ColumnMask() 注解,以及内置的 9 种脱敏规则,开箱即用:

/**
 * 内置的数据脱敏方式
 */
public class Masks {
    /**
     * 手机号脱敏
     */
    public static final String MOBILE = "mobile";
    /**
     * 固定电话脱敏
     */
    public static final String FIXED_PHONE = "fixed_phone";
    /**
     * 身份证号脱敏
     */
    public static final String ID_CARD_NUMBER = "id_card_number";
    /**
     * 中文名脱敏
     */
    public static final String CHINESE_NAME = "chinese_name";
    /**
     * 地址脱敏
     */
    public static final String ADDRESS = "address";
    /**
     * 邮件脱敏
     */
    public static final String EMAIL = "email";
    /**
     * 密码脱敏
     */
    public static final String PASSWORD = "password";
    /**
     * 车牌号脱敏
     */
    public static final String CAR_LICENSE = "car_license";
    /**
     * 银行卡号脱敏
     */
    public static final String BANK_CARD_NUMBER = "bank_card_number";
}

下面是一个使用 MyBatisFlexMapper 实现数据脱敏的示例代码:

import java.util.Base64;
import org.springframework.stereotype.Component;

@Component
public class UserMapper {

    /**
     * 隐藏密码
     *
     * @param user User 实体类
     * @return User 实体类
     */
    public User hidePassword(User user) {
        // 将密码用 Base64 加密后返回
        return new User("test_password", "test_username", "test_email",
                        Base64.getEncoder().encodeToString(user.getPassword().getBytes()));
    }
}

在上面的示例代码中,hidePassword 方法接收一个 User 实体类作为参数,将其密码使用 Base64 加密后返回,并替换掉原有的密码属性。

在使用时,我们可以在 MyBatis 的 XML 映射文件中定义 <result> 元素来实现数据脱敏:

<result property="password" column="password" />

其中,passwordUser 实体类中的密码属性,column 属性用来指定 password 属性在查询结果中的字段名。由于 MyBatis-Flex 会自动替换掉实体类中的属性,所以不需要使用其他脱敏方法。

除了 MyBatisFlexMapper,MyBatis 也提供了一些其他的数据脱敏方法,如 mybatis.typeHandlers.MyBatisFlexTypeHandlermybatis.generator.keywords.MapperSelectByPrimaryKey等。需要根据具体的业务需求来选择合适的方法进行数据脱敏。

Mybatis-Mate

Mybatis-Mate 是为 MyBatis-Plus 提供的企业级模块,旨在更敏捷优雅处理数据。不过,使用之前需要配置授权码。

@FieldSensitive("testStrategy")
private String username;

@Configuration
public class SensitiveStrategyConfig {

    /**
     * 注入脱敏策略
     */
    @Bean
    public ISensitiveStrategy sensitiveStrategy() {
        // 自定义 testStrategy 类型脱敏处理
        return new SensitiveStrategy().addStrategy("testStrategy", t -> t + "***test***");
    }
}

// 跳过脱密处理,用于编辑场景
RequestDataTransfer.skipSensitive();

自定义注解

  1. 首先定义一个用户脱敏的注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
// 指定序列化时使用 DesensitizationSerialize 这个自定义序列化类
// DesensitizationSerializ 我们后面会自定义
@JsonSerialize(using = DesensitizationSerialize.class)
public @interface Desensitization {
    /**
     * 脱敏数据类型,在MY_RULE的时候,startInclude和endExclude生效
     */
    DesensitizationTypeEnum type() default DesensitizationTypeEnum.MY_RULE;

    /**
     * 脱敏开始位置(包含)
     */
    int startInclude() default 0;

    /**
     * 脱敏结束位置(不包含)
     */
    int endExclude() default 0;
}
  1. 定义脱敏策略的枚举
public enum DesensitizationTypeEnum {
    //自定义
    MY_RULE,
    //用户id
    USER_ID,
    //手机号
    MOBILE_PHONE,
    //邮箱
    EMAIL,
}
  1. 自定义序列化类继承 JsonSerializer,实现 ContextualSerializer 接口,并重写 serialize()createContextual() 这两个方法。
/**
 * 自定义序列化类,用于数据脱敏处理
 * 支持多种脱敏类型,包括自定义规则。
 */
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizationSerialize extends JsonSerializer<String> implements ContextualSerializer {
    private DesensitizationTypeEnum type;
    private Integer startInclude;
    private Integer endExclude;

    @Override
    public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        switch (type) {
            // 自定义类型脱敏
            case MY_RULE:
                jsonGenerator.writeString(StrUtil.hide(str, startInclude, endExclude));
                break;
            // userId脱敏
            case USER_ID:
                jsonGenerator.writeString(String.valueOf(DesensitizedUtil.userId()));
                break;
            // 中文姓名脱敏
            case CHINESE_NAME:
                jsonGenerator.writeString(DesensitizedUtil.chineseName(String.valueOf(str)));
                break;
            // 省略其他数据类型脱敏
            // ......
        }
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            // 判断数据类型是否为String类型
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                // 获取定义的注解
                Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
                // 如果字段上没有注解,则从上下文中获取注解
                if (desensitization == null) {
                    desensitization = beanProperty.getContextAnnotation(Desensitization.class);
                }
                // 如果找到了注解,创建新的序列化实例
                if (desensitization != null) {
                    return new DesensitizationSerialize(desensitization.type(), desensitization.startInclude(), desensitization.endExclude());
                }
            }
            // 如果不是String类型,使用默认的序列化处理
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        // 如果beanProperty为null,返回默认的null值序列化处理
        return serializerProvider.findNullValueSerializer(null);
    }
}

这里可以对代码进行优化。可以将函数放进枚举类,进而避免使用 switch-case 语句,从而使代码更加简洁和易于维护。

优化后的DesensitizationTypeEnum:

public enum DesensitizationTypeEnum {
    // 自定义
    MY_RULE {
        @Override
        public String desensitize(String str, Integer startInclude, Integer endExclude) {
            return StrUtil.hide(str, startInclude, endExclude);
        }
    },
    // 用户id脱敏
    USER_ID {
        @Override
        public String desensitize(String str, Integer startInclude, Integer endExclude) {
            return String.valueOf(DesensitizedUtil.userId());
        }
    },
    // 手机号脱敏
    PHONE {
        @Override
        public String desensitize(String str, Integer startInclude, Integer endExclude) {
            return String.valueOf(DesensitizedUtil.mobilePhone(str));
        }
    },
    // 邮箱脱敏
    EMAIL {
        @Override
        public String desensitize(String str, Integer startInclude, Integer endExclude) {
            return String.valueOf(DesensitizedUtil.email(str));
        }
    };

    public abstract String desensitize(String str, Integer startInclude, Integer endExclude);
}

优化后的serialize 方法:

@Override
    public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(type.desensitize(str, startInclude, endExclude));
    }

只需要一行搞定,这样看起来是不是清晰了很多。

如果使用的序列化是 Fastjson 而不是默认的 Jackson,你可以创建一个自定义的 ValueFilter 来处理脱敏逻辑。

  1. 测试脱敏注解
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;

    @Desensitization(type = DesensitizationTypeEnum.EMAIL)
    private String email;

}
img
img