天天看點

太強了,一個注解搞定接口傳回資料脫敏!

下午惬意時光,突然産品小姐姐走到我面前,打斷我短暫的摸魚time,企圖與我進行深入交流,還好我早有防備沒有閃,打開瑞star的點單頁面,暗示沒有一杯coffee解決不了的需求,需求是某些接口傳回的資訊,涉及到敏感資料的必須進行脫敏操作,我思考一反,表示某問題,馬上安排。

思路

1.要做成可配置多政策的脫敏操作,要不然一個個接口進行脫敏操作,重複的工作量太多,很顯然違背了“多寫一行算我輸”的程式員規範,思來想去,定義資料脫敏注解和資料脫敏邏輯的接口, 在傳回類上,對需要進行脫敏的屬性加上,并指定對應的脫敏政策操作。

2.接下來我隻需要攔截控制器傳回的資料,找到帶有脫敏注解的屬性操作即可,一開始打算用@ControllerAdvice去實作,但發現需要自己去反射類擷取注解,當傳回對象比較複雜,需要遞歸去反射,性能一下子就會降低,于是換種思路,我想到平時使用的@JsonFormat,跟我現在的場景很類似,通過自定義注解跟字段解析器,對字段進行自定義解析,tql。面試寶典:https://www.yoodb.com 即将上線。

代碼

1. 自定義資料注解,并可以配置資料脫敏政策

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {

    DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;

}      

2. 自定義Serializer,參考jackson的StringSerializer,下面的示例隻針對String類型進行脫敏

public interface DataMaskingOperation {

    String MASK_CHAR = "*";
    
    String mask(String content, String maskChar);

}

public enum DataMaskingFunc {

     /**
     *  脫敏轉換器
     */
     NO_MASK((str, maskChar) -> {
        return str;
     }),
     ALL_MASK((str, maskChar) -> {
        if (StringUtils.hasLength(str)) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < str.length(); i++) {
                sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
            }
            return sb.toString();
        } else {
            return str;
        }
    });

    private final DataMaskingOperation operation;

    private DataMaskingFunc(DataMaskingOperation operation) {
        this.operation = operation;
    }

    public DataMaskingOperation operation() {
        return this.operation;
    }

}

public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
    private final DataMaskingOperation operation;

    public DataMaskingSerializer() {
        super(String.class, false);
        this.operation = null;
    }

    public DataMaskingSerializer(DataMaskingOperation operation) {
        super(String.class, false);
        this.operation = operation;
    }


    public boolean isEmpty(SerializerProvider prov, Object value) {
        String str = (String)value;
        return str.isEmpty();
    }

    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (Objects.isNull(operation)) {
            String content = DataMaskingFunc.ALL_MASK.operation().mask((String) value, null);
            gen.writeString(content);
        } else {
            String content = operation.mask((String) value, null);
            gen.writeString(content);
        }
    }

    public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
        this.serialize(value, gen, provider);
    }

    public JsonNode getSchema(SerializerProvider provider, Type typeHint) {
        return this.createSchemaNode("string", true);
    }

    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
        this.visitStringFormat(visitor, typeHint);
    }
}      

3. 自定義AnnotationIntrospector,适配我們自定義注解傳回相應的Serializer

@Slf4j
public class DataMaskingAnnotationIntrospector extends NopAnnotationIntrospector {

    @Override
    public Object findSerializer(Annotated am) {
        DataMasking annotation = am.getAnnotation(DataMasking.class);
        if (annotation != null) {
            return new DataMaskingSerializer(annotation.maskFunc().operation());
        }
        return null;
    }

}      

4. 覆寫ObjectMapper

@Configuration(
        proxyBeanMethods = false
)
public class DataMaskConfiguration {

    @Configuration(
            proxyBeanMethods = false
    )
    @ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
    static class JacksonObjectMapperConfiguration {
        JacksonObjectMapperConfiguration() {
        }

        @Bean
        @Primary
        ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
            ObjectMapper objectMapper = builder.createXmlMapper(false).build();
            AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
            AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntrospector());
            objectMapper.setAnnotationIntrospector(newAi);
            return objectMapper;
        }
    }

}      

5. 傳回對象加上注解

public class User implements Serializable {
    /**
     * 主鍵ID
     */
    private Long id;

    /**
     * 姓名
     */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String name;

    /**
     * 年齡
     */
    private Integer age;

    /**
     * 郵箱
     */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String email;

}      
作者:Mr_Mating