AOP实现对多数据类型字段进行加密解密

535 阅读4分钟

前言

使用AOP对指定的字段进行数据加密和解密。指定字段如果想被AOP监控到可以在字段上加自定义注解,考虑到字段类型不唯一,需要对不同数据类型进行不同方式的加密,比如密码是int类型,那加密之后必须也是int类型,要想实现这个功能肯定要进行判断,这里考虑用策略模式定义接口和多个实现类来实现;加密解密,网上搜了一下,针对String类型选择了常用的AES加密方式配合base64转码,至于其他数据类型后期有需要再选择不同的加密方式。

首先需要定义三个注解,分别是用在新增或更新方法上的加密注解、get方法上的解密注解和字段上的注解。在需要进行加密或解密的add或get方法上添加注解,AOP会对方法内的实体对象的字段进行扫描,看哪些字段需要进行加密或解密;其次我需要用策略模式实现对不同类型的字段进行不同方式的加解密,所以需要定义一个接口及若干实现类,这里为了演示方便只写一个String类型的实现类;最后就是要实现具体的加解密操作。

定义注解

该注解用在新增或更新方法上,AOP扫描到之后会对添加该注解的方法内的数据进行加密操作

 package com.th.dp.test;
 ​
 import java.lang.annotation.*;
 ​
 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface EncryptMethod {
 ​
 }

该注解用在get类方法上,AOP扫描到之后会对添加该注解的方法返回的数据进行解密操作

 package com.th.dp.test;
 ​
 import java.lang.annotation.*;
 ​
 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface DecryptMethod {
 ​
 }

该注解用在字段上,AOP扫描到之后会对添加该注解的字段进行具体的加解密操作

 package com.th.dp.test;
 ​
 import java.lang.annotation.*;
 ​
 @Target({ElementType.FIELD})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
 public @interface EncryptField {
     
 }

定义策略模式的接口和实现类

 package com.zhy.encrypt.utils;
 ​
 public interface EncryptComparator {
 ​
     Object encrypt(Object fieldValue);
 ​
     Object decrypt(Object fieldValue);
 }
 package com.zhy.encrypt.utils.impl;
 ​
 import com.zhy.encrypt.utils.EncryptComparator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.util.Base64Utils;
 ​
 import javax.crypto.Cipher;
 import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 ​
 @Configuration
 public class EncryptStringByAESImpl implements EncryptComparator {
 ​
     private static final Logger log = LoggerFactory.getLogger(EncryptStringByAESImpl.class);
     private static final String KEY_ALGORITHM = "AES";
     private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认的加密算法
 ​
     @Value("${encrypt.secretKey}")
     private String secretKey;
 ​
     /**
      * AES 加密操作
      *
      * @param content  待加密内容
      * @return 返回Base64转码后的加密数据
      */
     public String encrypt(Object content) {
         try {
             Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器
             byte[] byteContent = content.toString().getBytes("utf-8");
             cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secretKey));// 初始化为加密模式的密码器
             byte[] result = cipher.doFinal(byteContent);// 加密
             return Base64Utils.encodeToString(result);
         } catch (Exception ex) {
             log.error(ex.getStackTrace().toString());
         }
 ​
         return null;
     }
 ​
     /**
      * AES 解密操作
      *
      * @param content
      * @return
      */
     public String decrypt(Object content) {
         try {
             //实例化
             Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
             //使用密钥初始化,设置为解密模式
             cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secretKey));
             //执行操作
             byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content.toString()));
             return new String(result, "utf-8");
         } catch (Exception ex) {
             log.error(ex.getStackTrace().toString());
         }
 ​
         return null;
     }
 ​
     /**
      * 生成加密秘钥
      *
      * @return
      */
     private static SecretKeySpec getSecretKey(final String password) {
         //返回生成指定算法密钥生成器的 KeyGenerator 对象
         KeyGenerator kg = null;
         try {
             kg = KeyGenerator.getInstance(KEY_ALGORITHM);
             //AES 要求密钥长度为 128
             kg.init(128, new SecureRandom(password.getBytes()));
             //生成一个密钥
             SecretKey secretKey = kg.generateKey();
             return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
         } catch (NoSuchAlgorithmException ex) {
             log.error(ex.getStackTrace().toString());
         }
         return null;
     }
 }

定义切面类

该类主要负责扫描注解并调用具体实现类的加解密方法,具体逻辑看注释

 package com.zhy.encrypt.aop;
 ​
 import com.zhy.encrypt.annotation.EncryptField;
 import com.zhy.encrypt.utils.EncryptComparator;
 import com.zhy.encrypt.utils.impl.EncryptStringByAESImpl;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 ​
 import javax.annotation.PostConstruct;
 import java.lang.reflect.Field;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 ​
 @Aspect
 @Component
 public class EncryptFieldAop {
 ​
     private static final Logger log = LoggerFactory.getLogger(EncryptFieldAop.class);
 ​
     @Value("${encrypt.encryptMode}")
     private String encryptMode;
 ​
     @Autowired
     private EncryptStringByAESImpl encryptStringByAES;
 ​
     private Map<String, Object> map = new HashMap();
 ​
     @PostConstruct
     public void initial() {
         map.put("java.lang.String&AES", encryptStringByAES);
     }
 ​
     @Pointcut("@annotation(com.zhy.encrypt.annotation.EncryptMethod)")
     public void encryptPointCut() {
     }
 ​
     @Pointcut("@annotation(com.zhy.encrypt.annotation.DecryptMethod)")
     public void decryptPointCut() {
     }
 ​
     @Around("encryptPointCut()")
     public Object aroundToEn(ProceedingJoinPoint joinPoint) {
         Object responseObj = null;
         try {
             if (joinPoint.getArgs().length != 0) {
                 Object requestObj = joinPoint.getArgs()[0];
                 //方法进来就加密
                 handleEncrypt(requestObj);
                 //方法执行
                 responseObj = joinPoint.proceed();
             }
         } catch (NoSuchMethodException e) {
             e.printStackTrace();
             log.error("SecureFieldAop处理出现异常{}", e);
         } catch (Throwable throwable) {
             throwable.printStackTrace();
             log.error("SecureFieldAop处理出现异常{}", throwable);
         }
         return responseObj;
     }
     
     @Around("decryptPointCut()")
     public Object aroundToDe(ProceedingJoinPoint joinPoint) {
         Object responseObj = null;
         try {
             if (joinPoint.getArgs().length != 0) {
                 //方法执行
                 responseObj = joinPoint.proceed();
                 //方法返回就解密
                 handleDecrypt(responseObj);
             }
 ​
         } catch (NoSuchMethodException e) {
             e.printStackTrace();
             log.error("SecureFieldAop处理出现异常{}", e);
         } catch (Throwable throwable) {
             throwable.printStackTrace();
             log.error("SecureFieldAop处理出现异常{}", throwable);
         }
         return responseObj;
     }
 ​
     /**
      * 处理加密
      *
      * @param requestObj
      */
     private void handleEncrypt(Object requestObj) {
         if (Objects.isNull(requestObj)) {
             return;
         }
         //获取入参对象的字段
         Field[] fields = requestObj.getClass().getDeclaredFields();
         Arrays.stream(fields).filter(field -> field.isAnnotationPresent(EncryptField.class)).forEach(field -> {
             field.setAccessible(true);
             try {
                 Object  plaintextValue= field.get(requestObj);
                 EncryptComparator encryptComparator = (EncryptComparator) map.get(plaintextValue.getClass().getName()+"&"+encryptMode);
                 Object encryptValue = encryptComparator.encrypt(plaintextValue);
                 field.set(requestObj, encryptValue);
             } catch (IllegalAccessException e) {
                 e.printStackTrace();
             }
         });
     }
 ​
     /**
      * 处理解密
      *
      * @param responseObj
      */
     private Object handleDecrypt(Object responseObj) throws IllegalAccessException {
         if (Objects.isNull(responseObj)) {
             return null;
         }
         Field[] fields = responseObj.getClass().getDeclaredFields();
         Arrays.stream(fields).filter(field -> field.isAnnotationPresent(EncryptField.class)).forEach(field -> {
             field.setAccessible(true);
             try {
                 Object  plaintextValue= field.get(responseObj);
                 EncryptComparator encryptComparator = (EncryptComparator) map.get(plaintextValue.getClass().getName()+"&"+encryptMode);
                 Object encryptValue = encryptComparator.decrypt(plaintextValue);
                 field.set(responseObj, encryptValue);
             } catch (IllegalAccessException e) {
                 e.printStackTrace();
             }
         });
         return responseObj;
     }
 }

配置文件

application.yml

 encrypt:
   secretKey: zhy@911$
   encryptMode: AES

SPI

为了打包后能够让切面类在其他项目的spring容器中加载,需要在resource目录的META-INF下添加org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

  com.zhy.encrypt.aop.EncryptFieldAop

当然,也可以在Spring Boot主类上使用 @Import 注解注入切面类或将上述文件替换为spring.factories文件,内容保持不变

加密后的数据如何进行模糊查询

这里针对字符型数据整理了几个实现方案

查询出该字段的所有加密数据,在内存中解密为明文再和前端传过来的参数比对

优点是实现简单,缺点也很明显,数据量大的情况下会产生灾难性后果

存储一份明文映射

在数据库中增加一列存储明文,如此一来加密也就没有必要了

明文分词加密再保存

这是相对来说比较可行的方案,将明文按照规定大小进行分词,将每个分词进行加密,最后拼接保存,缺点是前端参数不能小于分词长度,且最终入库的密文长度较长