数据脱敏设计方案
什么是数据脱敏
数据脱敏(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" />
其中,password
是 User
实体类中的密码属性,column
属性用来指定 password
属性在查询结果中的字段名。由于 MyBatis-Flex 会自动替换掉实体类中的属性,所以不需要使用其他脱敏方法。
除了 MyBatisFlexMapper
,MyBatis 也提供了一些其他的数据脱敏方法,如 mybatis.typeHandlers.MyBatisFlexTypeHandler
和 mybatis.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();
自定义注解
- 首先定义一个用户脱敏的注解。
@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;
}
- 定义脱敏策略的枚举
public enum DesensitizationTypeEnum {
//自定义
MY_RULE,
//用户id
USER_ID,
//手机号
MOBILE_PHONE,
//邮箱
EMAIL,
}
- 自定义序列化类继承
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
来处理脱敏逻辑。
- 测试脱敏注解
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
@Desensitization(type = DesensitizationTypeEnum.EMAIL)
private String email;
}