这道面试题是一位读者参加美团面试被拷打项目时问到的。
这里以这位读者项目中实际使用的方法为例进行介绍。
后端返回数据给前端的时候,一般需要对敏感词脱敏,类似于下面这样:
脱敏的规则有很多种,例如:
替换(常用):将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(*)或其他字符。 删除:将敏感数据中的部分内容随机删除。例如,将电话号码的随机 3 位数字进行删除。 重排:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。 加噪:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。 ......
这里以最常用的替换为例进行介绍,这也是我的项目用到的方法。
我是利用 Hutool 提供的 DesensitizedUtil
脱敏工具类配合 Jackson 通过注解的方式完成数据脱敏的。
如果不想引入 Hutool 的话,也可以自己实现一个脱敏工具类,实现逻辑非常简单。
DesensitizedUtil
脱敏工具类支持用户 ID、中文姓名、身份证号、座机号、手机号、电子邮件、银行卡号等脱敏数据类型,基本覆盖了常见的敏感信息。
DesensitizedUtil
脱敏工具类的脱敏规则是隐藏掉信息中的一部分关键信息用*
代替,例如:
身份证号:原始值 51343620000320711X
,脱敏后5***************1X
手机号:原始值 18049531999
,脱敏后180****1999
银行卡号:原始值 6217000130008255666
,脱敏后6217 **** **** *** 5666
......
除了支持常见的脱敏数据类型之外,Hutool 还提供了自定义隐藏方法StrUtil#hide
。这个方法实际上是 CharSequenceUtil
实现的,StrUtil
继承了CharSequenceUtil
。
因为我的项目是基于 Spring Boot 开发的,因此可以利用 Spring Boot 自带的 Jackson 自定义序列化实现,在 JSON 进行序列化渲染给前端时,进行脱敏。
下面是简化后的核心步骤:
1、我定义了一个用于脱敏的 Desensitization
注解。
@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;
}
DesensitizationTypeEnum
是脱敏策略的枚举:
public enum DesensitizationTypeEnum {
//自定义
MY_RULE,
//用户id
USER_ID,
//手机号
MOBILE_PHONE,
//邮箱
EMAIL,
// 省略其他枚举字段
// ...
}
2、自定义序列化类继承 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
语句,从而使代码更加简洁和易于维护。
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());
}
},
MOBILE_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);
}
这样的话,一行代码即可实现调用:
jsonGenerator.writeString(type.desensitize(str, startInclude, endExclude));
如果使用的序列化是 Fastjson 而不是默认的 Jackson,你可以创建一个自定义的 ValueFilter
来处理脱敏逻辑。
这里只是以 Jackson 和 Fastjson 为例说明,其他常见的序列化实现都有对应的解决方法。
3、经过上面两步之后就可以使用脱敏注解了。
在对应的字段上添加上脱敏注解即可:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
// 演示自定义脱敏
@Desensitization(type = DesensitizationTypeEnum.MY_RULE, startInclude = 4, endExclude = 7)
private String userid;
@Desensitization(type = DesensitizationTypeEnum.MOBILE_PHONE)
private String phone;
@Desensitization(type = DesensitizationTypeEnum.EMAIL)
private String email;
}
输出示例:
{
"userid": "user***56",
"phone": "181****8155",
"email": ":*************@163.com"
}