美团面试:如何在项目中实现敏感词脱敏处理的最佳实践与步骤详解

这道面试题是某位读者在美团面试过程中被询问到的,涉及到项目中敏感词脱敏的处理方法。

图片在实际的项目中,后端在向前端返回数据时,通常需要对敏感词进行脱敏处理,具体示例如下:

图片其中,脱敏的方法有多种,常见的有:

  • 替换:这是最常用的方法,通过将敏感信息中的特定字符替换为其他字符。例如,信用卡号的中间几位可以被替换为星号(*)。
  • 删除:从敏感信息中随机删除一部分内容。例如,删除电话号码中的随机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序列化时进行脱敏处理。

实现步骤

  1. 定义脱敏注解

首先,我定义了一个用于脱敏的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,  
    // 省略其他枚举字段  
}  
  1. 自定义序列化类

接下来,编写继承于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来处理脱敏逻辑。其他序列化实现也都有各自的解决方案。

  1. 使用脱敏注解

完成上述步骤后,可以在需要脱敏的字段上添加注解:

@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"  
}