美团面试:如何在项目中实现敏感词脱敏处理的最佳实践与步骤详解
这道面试题是某位读者在美团面试过程中被询问到的,涉及到项目中敏感词脱敏的处理方法。
在实际的项目中,后端在向前端返回数据时,通常需要对敏感词进行脱敏处理,具体示例如下:
其中,脱敏的方法有多种,常见的有:
- 替换:这是最常用的方法,通过将敏感信息中的特定字符替换为其他字符。例如,信用卡号的中间几位可以被替换为星号(*)。
- 删除:从敏感信息中随机删除一部分内容。例如,删除电话号码中的随机3位数字。
- 重排:打乱原数据中某些字符或字段的顺序,比如身份证号码的随机位互换。
- 加噪:在数据中注入一些误差或噪音,以达到脱敏的效果。例如,在敏感信息中添加随机字符。
- ......
在这里,我们将以最常用的替换方法为例进行详细讲解,这也是我项目中使用的方法。
我使用了Hutool提供的DesensitizedUtil
脱敏工具类,结合Jackson通过注解的方式完成数据脱敏。如果不想引入Hutool,也可以自定义实现一个脱敏工具类,逻辑非常简单。
DesensitizedUtil
支持多种敏感数据类型的脱敏,如用户ID、中文姓名、身份证号、座机号、手机号、电子邮件、银行卡号等,基本覆盖了常见的敏感信息。
该工具类的脱敏规则是隐藏信息中的一部分关键信息,用*
替代。例如:
- 身份证号:原始值
51343620000320711X
,脱敏后5***************1X
- 手机号:原始值
18049531999
,脱敏后180****1999
- 银行卡号:原始值
6217000130008255666
,脱敏后6217 **** **** *** 5666
- ......
除了常见的脱敏数据类型外,Hutool还提供了自定义隐藏方法StrUtil#hide
。这个方法实际上是由CharSequenceUtil
实现,而StrUtil
继承了CharSequenceUtil
。
由于我的项目是基于Spring Boot开发的,因此可以利用Spring Boot自带的Jackson自定义序列化实现,在JSON序列化时进行脱敏处理。
实现步骤
- 定义脱敏注解
首先,我定义了一个用于脱敏的Desensitization
注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerialize.class)
public @interface Desensitization {
DesensitizationTypeEnum type() default DesensitizationTypeEnum.MY_RULE;
int startInclude() default 0;
int endExclude() default 0;
}
其中,DesensitizationTypeEnum
是脱敏策略的枚举:
public enum DesensitizationTypeEnum {
MY_RULE,
USER_ID,
MOBILE_PHONE,
EMAIL,
// 省略其他枚举字段
}
- 自定义序列化类
接下来,编写继承于JsonSerializer
的自定义序列化类,重写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;
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) {
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());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
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);
}
},
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
来处理脱敏逻辑。其他序列化实现也都有各自的解决方案。
- 使用脱敏注解
完成上述步骤后,可以在需要脱敏的字段上添加注解:
@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"
}