Desensitization mode of Controller interface based on AOP

preface

Our background system has received the demand for data desensitization, which requires desensitization display of important information such as mobile phone number and receiving address on some associated pages. That's why we have this desensitization scheme

Common scheme

Scheme I

Add annotations to key DTO s and Vos. When these objects return the caller through the interface, the value of the annotation field will be masked
advantage:

  1. As long as comments are added, all interfaces that return this object will be masked, and there is no need to write code separately for each interface

Disadvantages:

  1. Cannot refine control for specific interfaces
  2. The returned front-end DTO object is defined in other micro services, so it is inconvenient to add comments on the field

Scheme II

Add notes in the controller interface to identify specific fields that need to be masked
advantage:

  1. Fine control the return result mask of each interface
  2. The code of the type of the returned object does not need to be modified

Disadvantages:

  1. Each interface needs to be annotated,
  2. Need to write AOP facet class

Programme III

The front end is shielded and displayed by itself
Disadvantages: belong to their own scheme to deceive themselves

On the whole, our background system references too many DTO objects of other microservices and returns them directly to the front end. Scheme 1 cannot be adopted, but scheme 2 is finally used

Implementation mode

The general process of a request is as follows:

I adopt scheme 2 to realize:

  • When the result is returned, get the annotation value from the facet class and put the annotation value into ThreadLocal. The annotation uses the syntax identification mask field of jsonPath
  • During serialization, the annotation in ThreadLocal is obtained, and the corresponding field value is shielded according to the annotation information. The specific shielding uses fastjson

be careful:

  • When the response is returned, the section class will get the annotation and put it into ThreadLocal. Because http requests may be sent in the controller logic, a set of logic in the figure above will be embedded in the controller. Putting it into ThreadLocal too early will affect each other
  • When serializing, get the configuration information from ThreadLocal, and then use the jsonpath of fastjson to modify the corresponding value

Two annotation classes

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveWord {
    Sensitive[] value();
}
import java.lang.annotation.*;

import static com.youdao.athena.starfire.core.support.sensitive.DesensitizedType.PASSWORD;

@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    /**
     * json path Identification of
     * @return
     */
    String jsonPath();

    /**
     * Data classification of desensitized fields. The default is password type desensitization,   
     * @return
     */
     DesensitizedType desensitizedType() default PASSWORD;
}

Section class

Use anonymous inner classes and reflection to obtain annotation information and put it into ThreadLocal

    @Bean
    public Advisor pointcutAdvisor() {
        MethodInterceptor interceptor = methodInvocation -> {
            Object proceed = methodInvocation.proceed();

            SensitiveWord annotation = AnnotationUtil.getAnnotation(methodInvocation.getMethod(), SensitiveWord.class);
            if (annotation != null && !this.isExport(methodInvocation)) {
                Sensitive[] value = annotation.value();
                sensitiveThreadLocal.set(value);
            }
            return proceed;
        };

        AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, SensitiveWord.class);
        // Configure enhanced class advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        advisor.setAdvice(interceptor);
        return advisor;
    }

Desensitization serialization class

  • MappingJackson2HttpMessageConverter overrides the writeInternal method of this class and performs desensitization during serialization
  • The replace method is used for jsonPath replacement. The desensitized util class used is the desensitization related tool class of hutool
    @Bean
    public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter() {
            /**
             * Rewrite the method to desensitize sensitive information
             * @param object
             * @param type
             * @param outputMessage
             * @throws IOException
             * @throws HttpMessageNotWritableException
             */
            @Override
            protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                Sensitive[] values = InterceptorConfig.sensitiveThreadLocal.get();
                if (values == null) {
                    super.writeInternal(object, type, outputMessage);
                    return;
                }
                JSONObject jsonObject = (JSONObject) JSON.toJSON(object);
                for (Sensitive sensitive : values) {
                    String jsonPath = sensitive.jsonPath();
                    replace(jsonObject, jsonPath);
                }
                super.writeInternal(jsonObject, type, outputMessage);
                InterceptorConfig.sensitiveThreadLocal.remove();
            }
        };

        return mappingJackson2HttpMessageConverter;
    }

    /**
     * Sensitive data replacement
     *
     * @param jsonObject
     * @param jsonPath
     */
    private static void replace(JSONObject jsonObject, String jsonPath) {
        if (JSONPath.contains(jsonObject, jsonPath)) {
            int index = jsonPath.lastIndexOf("[*]");
            if (index > -1) {
                String prefix = StrUtil.subPre(jsonPath, index);
                String suffix = StrUtil.subSuf(jsonPath, index + 3);
                Object eval = JSONPath.eval(jsonObject, prefix);
                JSONArray jsonArray = (JSONArray) eval;
                int size = jsonArray.size();
                for (int i = 0; i < size; i++) {
                    String indexJsonPath = StrUtil.strBuilder().append(prefix).append("[").append(i).append("]").append(suffix).toString();
                    String desensitized = Convert.toStr(JSONPath.eval(jsonObject, indexJsonPath));
                    if (StrUtil.isBlank(desensitized)) {
                        continue;
                    }
                    desensitized = DesensitizedUtil.desensitized(desensitized, DesensitizedType.MOBILE_PHONE);
                    JSONPath.set(jsonObject, indexJsonPath, desensitized);
                }
            } else {
                Object eval = JSONPath.eval(jsonObject, Convert.toStr(jsonPath));
                String desensitized = DesensitizedUtil.desensitized(Convert.toStr(eval), DesensitizedType.MOBILE_PHONE);
                JSONPath.set(jsonObject, jsonPath, desensitized);
            }
        }
    }

Interface class

  • The sentiveword annotation indicates that this interface needs to be desensitized
  • Sensitive has several fields that need desensitization, so it configures annotations several times
    @SensitiveWord({
            @Sensitive(jsonPath = "$.body.courseUsers[*].mobile",desensitizedType = MOBILE_PHONE),
            @Sensitive(jsonPath = "$.body.courseUsers[*].address",desensitizedType = ADDRESS),
    })
    @PostMapping("/query/list")
    public WebResponse queryList(UserDTO userDTO, @RequestBody CourseUserQueryParam queryParam) {
   
		..... Business code omitted
    }

Interface call

The call result is shown in the figure below

Trampled pit

  1. In front of the aspect proxy controller method, the annotation is put into threadLocal. As a result, the controller method will call other microservice interfaces through RPC(http) and will also use MappingJackson2HttpMessageConverter for serialization. As a result, the annotation value in threadLocal is used and released by rpc method

  2. jsonPath ignores calling null when getting the value of the specified location, resulting in misplaced shielding

Tags: Java Spring Spring Cloud RESTful

Posted on Mon, 06 Sep 2021 19:24:49 -0400 by dirty_n4ppy