SpringBoot custom annotation + interceptor implementation

In actual production projects, sensitive data such as ID card information, mobile phone number and real name often need to be encrypted and stored in the database. However, manual encryption and decryption of sensitive information in business code is not elegant, and there may even be wrong encryption, missing encryption, and business personnel need to know the actual encryption rules.

This article will introduce the detailed process of intercepting and encrypting sensitive data before storage in the form of springboot+mybatis interceptor + custom annotation.

1, What is Mybatis Plugin

In the official document of mybatis, the introduction of Mybatis plugin is as follows:

MyBatis allows you to intercept calls at some point during the execution of mapped statements. By default, MyBatis allows plug-ins to intercept method calls, including:

//Statement execution interception
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

//  Intercept during parameter acquisition and setting
ParameterHandler (getParameterObject, setParameters)

//  Intercept the returned results
ResultSetHandler (handleResultSets, handleOutputParameters)

//sql statement interception
StatementHandler (prepare, parameterize, batch, update, query)

In short, in the whole cycle of sql execution, we can cut into a certain point to deal with the parameters of sql, the result set of sql execution, the sql statement itself, etc. Based on this feature, we can use it to uniformly encrypt the data we need to encrypt (this is how the paging plug-in pageHelper implements database paging query).

2, Implement annotation based encryption and decryption interceptor for sensitive information

2.1 implementation ideas

For data encryption and decryption, there should be two interceptors to intercept the data

Refer to the official documentation, so here we should use the ParameterHandler interceptor to encrypt the incoming parameters

Decrypt the output parameter using the ResultSetHandler interceptor.

The target fields to be encrypted and decrypted may need to be changed flexibly. At this time, we define an annotation to annotate the fields to be encrypted, so we can cooperate with the interceptor to encrypt and decrypt the required data.

The interceptor interface of mybatis has the following methods to implement.

public interface Interceptor {
 
  //Main parameters interception method
  Object intercept(Invocation invocation) throws Throwable;
 
  //mybatis plug-in chain
  default Object plugin(Object target) {return Plugin.wrap(target, this);}
 
  //Custom plug-in profile method
  default void setProperties(Properties properties) {}
 
}

2.2 notes for defining sensitive information to be encrypted and decrypted

Define annotations for sensitive information classes (such as entity class POJO\PO)

/**
 * Annotation of sensitive information class
 */
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}

Annotations that define sensitive fields in sensitive information classes

/**
 * Annotation of sensitive fields in sensitive information class
 */
@Inherited
@Target({ ElementType.Field })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}

2.3 define encryption interface and its implementation class

Define the encryption interface to facilitate the expansion of encryption methods in the future (for example, the expansion of AES encryption algorithm supports PBE algorithm, which only needs to be specified during injection)

public interface EncryptUtil {
    
    /**
     * encryption
     *
     * @param declaredFields paramsObject Declared fields
     * @param paramsObject   mapper Instance of paramtype in
     * @return T
     * @throws IllegalAccessException Field inaccessible exception
     */
     <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}

EncryptUtil's AES encryption implementation class. Here AESUtil is a self encapsulated AES encryption tool, which can be encapsulated by small partners. It is not provided in this article.

@Component
public class AESEncrypt implements EncryptUtil {
    
    @Autowired
    AESUtil aesUtil;
 
    /**
     * encryption
     *
     * @param declaredFields paramsObject Declared fields
     * @param paramsObject   mapper Instance of paramtype in
     * @return T
     * @throws IllegalAccessException Field inaccessible exception
     */
    @Override
    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
        for (Field field : declaredFields) {
            //Retrieve all fields annotated by EncryptDecryptField
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //For the time being, only String type encryption is implemented
                if (object instanceof String) {
                    String value = (String) object;
                    //encryption    Here I use the custom AES encryption tool
                    field.set(paramsObject, aesUtil.encrypt(value));
                }
            }
        }
        return paramsObject;
    }
}

2.4 realize the entry and participation of the secret interceptor

The org.apache.ibatis.plugin.Interceptor interceptor interceptor interface in the Myabtis package requires us to implement the following three methods

public interface Interceptor {
 
  //Core interception logic
  Object intercept(Invocation invocation) throws Throwable;
  
  //Interceptor chain 
  default Object plugin(Object target) {return Plugin.wrap(target, this);}
 
  //Custom profile actions
  default void setProperties(Properties properties) { }
 
}

Therefore, referring to the example in the official document, we customize an incoming secret interceptor.

@The Intercepts annotation enables the interceptor, and the @ Signature annotation defines the actual type of the interceptor.

@In Signature

  • The type attribute specifies that the current interceptor uses StatementHandler, ResultSetHandler, ParameterHandler, and one of the executors

  • The method attribute specifies the specific methods using the above four types (you can view their methods inside the class).

  • The args property specifies the precompiled statement

Here we use   Parameterhandler. Setparameters() method to intercept the instance of paramsType in mapper.xml (that is, execute the interceptor in each mapper statement containing paramsType attribute to intercept the instance of paramsType)

/**
 * Encryption interceptor
 * Note that the @ Component annotation must be added
 *
 * @author : tanzj
 * @date : 2020/1/19.
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {
 
    private final EncryptDecryptUtil encryptUtil;
 
    @Autowired
    public EncryptInterceptor(EncryptDecryptUtil encryptUtil) {
        this.encryptUtil = encryptUtil;
    }
 
    @Override
   
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature   Specified   type=   parameterHandler   After that, here   invocation.getTarget()   Is parameterhandler  
        //If ResultSetHandler is specified  , Here, it can be forcibly converted to ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        //  Get the parameter object, i.e   mapper   in   paramsType   Examples of
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //Take out instance
        Object parameterObject = parameterField.get(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();
            //Verify whether the class of this instance is annotated by @ SensitiveData
            SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
            if (Objects.nonNull(sensitiveData)) {
                //Take out all fields of the current class and pass in the encryption method
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                encryptUtil.encrypt(declaredFields, parameterObject);
            }
        }
        return invocation.proceed();
    }
 
    /**
     * Remember to configure, otherwise the current interceptor will not join the interceptor chain
     */
    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }
 
    //Write custom configuration. If there is no custom configuration, this method can be directly blank
    @Override
    public void setProperties(Properties properties) {
    }
}

This completes the custom encryption interception encryption.

2.5 define decryption interface and its implementation class

Decrypt the interface, where result is the instance of resultType in mapper.xml.

public interface DecryptUtil {
 
    /**
     * decrypt
     *
     * @param result resultType Examples of
     * @return T
     * @throws IllegalAccessException Field inaccessible exception
     */
     <T> T decrypt(T result) throws IllegalAccessException;
    
}

Decryption interface AES tool decryption implementation class

public class AESDecrypt implements DecryptUtil {
    
    @Autowired
    AESUtil aesUtil;
    
    /**
     * decrypt
     *
     * @param result resultType Examples of
     * @return T
     * @throws IllegalAccessException Field inaccessible exception
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //Get the class of resultType
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //Retrieve all fields annotated by EncryptDecryptField
            SensitiveField sensitiveField = field.getAnnotation(SensitiveField.class);
            if (!Objects.isNull(sensitiveField)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //Only decryption of String is supported
                if (object instanceof String) {
                    String value = (String) object;
                    //Decrypt the annotated fields one by one
                    field.set(result, aesUtil.decrypt(value));
                }
            }
        }
        return result;
    }
}

2.6 define parameter decryption interceptor

@Slf4j
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {
 
    @Autowired
    DecryptUtil aesDecrypt;
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //Fetch query results
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //Based on selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //Decrypt one by one
                    aesDecrypt.decrypt(result);
                }
            }
        //Based on selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                aesDecrypt.decrypt(resultObject);
            }
        }
        return resultObject;
    }
 
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }
 
 
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
 
    @Override
    public void setProperties(Properties properties) {
 
    }
}

This completes the configuration of the decryption interceptor.

3. Annotate the fields to be encrypted and decrypted in the entity class

At this time, specify paramType=User resultType=User in mapper to realize the encryption and decryption operation based on mybatis interceptor.

Keywords: Java Spring Spring Boot

Added by loci on Tue, 23 Nov 2021 05:51:55 +0200