Encryption of sensitive fields based on Mybatis layer

Posted by clarky08 on Mon, 07 Mar 2022 17:41:38 +0100

In the SpringBoot project, how to gracefully implement the encryption and decryption of sensitive fields by user-defined annotation + interceptor

 We are often faced with manual encryption of some identity information or phone numbers, real names and other sensitive information, which is not only very bloated but also very elegant. There will even be wrong encryption, building encryption, developers need to know the actual encryption rules and so on.

This article will tell you how to use SpringBoot + Mybatis interceptor + annotation to complete data encryption based on mapper level

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
//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, during the whole sql execution cycle, we can cut into the sql parameters, sql execution result set, sql statement itself and other aspects at any point. Therefore, based on this feature, we can use it to uniformly encrypt the data we need to encrypt

2, Implement annotation based sensitive information encryption interceptor

2.1 implementation ideas

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

Refer to the official documents, so here we should use the ParameterHandler interceptor to encrypt the incoming parameters
Decrypt the output parameter using the ResultSetHandler interceptor.

The target needs to be decrypted, and the decrypted fields may need to be changed flexibly. At this time, we define an annotation to annotate the fields that need to be encrypted. Then we can cooperate with the interceptor to wake up the encryption and decryption operation of the required data. Then we can see that the interceptor of mybatis needs to implement the following methods

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.1 notes for defining sensitive information to be encrypted and decrypted

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

/**
 * @author kunBoy
 * @create 2021-04-13 10:23
 * Annotation of sensitive information class
 */
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}

Annotations defining sensitive fields

/**
 * @author kunBoy
 * @create 2021-04-13 10:23
 * Annotation of sensitive fields
 */
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveFiled {
}

2.3 define encryption interface machine implementation class

The encryption interface shall be used 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 by injection)

/**
 * @author kunBoy
 * @create 2021-04-13 10:34
 */
public interface EncryptUtil {

    /**
     *
     * @param aesFields paramsObject Declared field
     * @param paramsObject mapper Instance of paramtype in
     * @param <T>
     * @return
     * @throws IllegalAccessException Field inaccessible exception
     * This is to write this interface and expand its encryption type in the future
     */
    <T> T aesEncrypt(Field[] aesFields, T paramsObject) throws IllegalAccessException;
}

AESUtil tool class (can also be self encapsulated)

/**
 * @version V1.0
 * @desc AES Encryption tool class
 */
@Component
public class AESUtil {

    private static final String KEY_ALGORITHM = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//Default encryption algorithm

    /**
     * AES Encryption operation
     *
     * @param content  Content to be encrypted
     * @return Returns the encrypted data after Base64 transcoding
     */
    public static String encrypt(String content, String key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// Create cipher

            byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);

            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// Cipher initialized to encryption mode

            byte[] result = cipher.doFinal(byteContent);// encryption

            //Base64 is a representation of binary data based on 64 printable characters.
            return Base64Utils.encodeToString(result);//Return through Base64 transcoding
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    /**
     * AES Decryption operation
     *
     * @param content
     * @return
     */
    public static String decrypt(String content, String key) {

        try {
            //instantiation 
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);

            //Use key initialization and set it to decryption mode
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));

            //Perform operations
            //Base64 is a representation of binary data based on 64 printable characters.
            byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));

            return new String(result, StandardCharsets.UTF_8);
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     * Generate encryption key
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(String key) {
        //Returns the KeyGenerator object that generates the specified algorithm key generator
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
            //AES requires a key length of 128
            kg.init(128, new SecureRandom(key.getBytes()));
            //Generate a key
            SecretKey secretKey = kg.generateKey();
            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// Convert to AES private key
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
}

Implementation class

/**
 * Encryption tool class
 * @author kunBoy
 * @create 2021-04-13 10:39
 */
@Component
public class AESEncrypt implements EncryptUtil {

    @Value("${aes.key}")
    private String key;

    /**
     *
     * @param aesFields paramsObject Declared field
     * @param paramsObject mapper Instance of paramtype in
     * @param <T>
     * @return
     * @throws IllegalAccessException Field inaccessible exception
     */
    @Override
    public <T> T aesEncrypt(Field[] aesFields, T paramsObject) throws IllegalAccessException {
        for (Field aesField : aesFields) {
            //Retrieve all fields annotated by EncryptDecryptFiled
            SensitiveFiled filed = aesField.getAnnotation(SensitiveFiled.class);
            if (!Objects.isNull(filed)) {
                //Set the accessible flag of this object to the Boolean value indicated. A value of true indicates that the Java language access check should be canceled when the reflected object is used.
                aesField.setAccessible(true);
                Object object = aesField.get(paramsObject);
                //For the time being, only String type is encrypted
                if (object instanceof String) {
                    String value = (String) object;
                    //Start encrypting fields using custom AES encryption tools
                    aesField.set(paramsObject, AESUtil.encrypt(value, key));
                }
            }
        }
        return paramsObject;
    }
}

2.4 implementation of incoming secret interceptor

As mentioned earlier, if we need to customize the interceptor, we need to implement the three methods given by Mybatis

Here we use ParameterHandler.setParameters()Method, intercepted mapper.xml in paramsType Instances of (i.e. in each case parameType attribute mapper Statement, execute the interceptor, and paramsType (intercept the instance of)
/**
 * Encryption interceptor
 * @author kunBoy
 * @create 2021-04-13 11:06
 */
@Slf4j
@Component
/**
 * @Intercepts Annotation open interceptor
 * type Property specifies that the current interceptor uses StatementHandler, ResultSetHandler, ParameterHandler, and one of the executors
 * method Property specifies the specific methods using the above four types (you can view their methods inside class).
 * args Property specifies the precompiled statement
 */
@Intercepts({
        //@The Signature annotation defines the actual type of interceptor
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {

    private final AESEncrypt encryptUtil;

    @Autowired
    public EncryptInterceptor(AESEncrypt encryptUtil) {
        this.encryptUtil = encryptUtil;
    }
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@After type= parameterHandler is specified in the Signature, the invocation here Gettarget () is parameterHandler
        //If ResultSetHandler is specified, it can be forcibly converted to ResultSetHandler here
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        //Get the parameter object, that is, the instance of paramsType in mapper
        Field paramsFiled = parameterHandler.getClass().getDeclaredField("parameterObject");
        //Set the accessible flag of this object to the Boolean value indicated. A value of true indicates that the Java language access check should be canceled when the reflected object is used.
        paramsFiled.setAccessible(true);
        //Take out the instance
        Object parameterObject = paramsFiled.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.aesEncrypt(declaredFields, parameterObject);
            }
        }
        //Gets the return value of the original method
        return invocation.proceed();
    }

    /**
     * Be sure to configure and add this interceptor to the interceptor chain
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

This completes the custom encryption interceptor

2.5 define decryption interface and its implementation class

Decrypt the interface, where result is mapper Instance of resultType in XML

/**
 * @author kunBoy
 * @create 2021-04-13 11:44
 */

public interface DecryptUtil {

    /**
     * decrypt
     *
     * @param result
     * @param <T>
     * @return
     * @throws IllegalAccessException
     */
    <T> T decrypt(T result) throws IllegalAccessException;
}
/**
 * @author kunBoy
 * @create 2021-04-13 11:45
 */
@Component
public class AESDecrypt implements DecryptUtil {

    @Value("${aes.key}")
    private String key;


    /**
     * decrypt
     *
     * @param result
     * @param <T>
     * @return
     * @throws IllegalAccessException
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //Get the class of resultType
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            //Remove all fields annotated with EncryptDecryptFiled
            SensitiveFiled sensitiveFiled = declaredField.getAnnotation(SensitiveFiled.class);
            if (!Objects.isNull(sensitiveFiled)) {
                //Set the accessible flag of this object to the Boolean value indicated. A value of true indicates that the Java language access check should be canceled when the reflected object is used.
                declaredField.setAccessible(true);
                //The result here is equivalent to the accessor of the field
                Object object = declaredField.get(result);
                //Only String decryption is supported
                if (object instanceof String) {
                    String value = (String) object;
                    //Decrypt the comments one by one in this paragraph
                    declaredField.set(result, AESUtil.decrypt(value, key));
                }
            }
        }
        return result;
    }
}

2.6 define parameter decryption interceptor

/**
 * @author kunBoy
 * @create 2021-04-13 11:56
 */
@Slf4j
@Component
/**
 * This is to decrypt the found string result set, so it is ResultSetHandler
 * args Specifies the precompiled statement
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {

    @Autowired
    AESDecrypt aesDecrypt;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //Retrieve the results of the query
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //selectList based
        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;
    }

    /**
     * A method of empty judgment for single result set
     * @param object
     * @return
     */
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }

    /**
     * Add this filter to the filter chain
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

So far, all configuration work has been completed.

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

@ApiModel(value="")
@Data
@SensitiveData
@TableName("archive_archive")
public class Archive implements Serializable {

private static final long serialVersionUID = 1L;

    @SensitiveFiled
    @TableId(type = IdType.ASSIGN_ID)
    private String archiveId;

    private String archiveName;

    private String archiveVersion;

    private String archiveNum;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8", locale = "zh")
    private Date enterTime;

    private String enterPerson;

    @SensitiveFiled
    private String moduleId;

    @SensitiveFiled
    private String orgId;


}

A clumsy work, also hope to help families in need

Topics: Mybatis