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