SpringBoot项目集成数据脱敏(密码加密)功能

代码连接【https://gitee.com/pengmqqq/sensitive-data-encryption】

介绍

后端敏感数据加密的一些解决方案,包括:

  1. 配置文件敏感数据加解密
  2. 前端传输敏感数据加解密
  3. 数据库获取的敏感数据加解密
软件架构

配置文件数据脱敏: Jasypt + AES

前后端传输以及数据库存储数据脱敏:AOP + AES

使用说明
  1. 配置文件数据脱敏

    将需要脱敏的数据进行加密之后再放入配置文件(注意要使用解密算法配套的加密算法)例如:

    test:
      password: Enc(tKWTx+XSlLlJFdLOIQPKYQ==)
    
  2. 前后端传输以及数据库存储数据脱敏:

    在需要加密/解密的属性/参数上增加注解 @EncryptField

    @Data
    @Accessors(chain = true)
    public class User {
        private Integer id;
        private String name;
        @EncryptField
        private String phone;
        @EncryptField
        private String email;
        private Integer age;
    }
    

    在需要对参数加密的方法上增加注解 @NeedEncrypt

    @NeedEncrypt
    public void addAll(List<User> user) {
        users.addAll(user);
        System.out.println("");
    }
    

    在需要对返回值解密的方法上增加注解 @NeedDecrypt

    例如某些需要访问第三方平台的操作,从数据库取到的是加密的数据,代码中需要进行解密再发送给第三方平台进行认证

    @NeedDecrypt
    public List<User> findAll() {
        ArrayList<User> list = new ArrayList<>(users);
        return list;
    }
    
实现方案
配置文件数据脱敏:
  • pom文件引入依赖:

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
    
    <dependency>
        <groupId>com.github.ulisesbocchiogroupId>
        <artifactId>jasypt-spring-boot-starterartifactId>
        <version>3.0.4version>
    dependency>
    
  • 配置文件 application.yml 新增jasypt相关配置:

    jasypt:
      encryptor:
        property:
          # 算法识别的前后缀,默认ENC(),包含在前后缀的加密信息,会使用指定算法解密
          prefix: Enc(
          suffix: )
        bean: desencrypt  # 指定自定义加密算法的bean
    
    test:
      password: Enc(tKWTx+XSlLlJFdLOIQPKYQ==)  # 加密数据
    
  • 新增自定义算法类:

    @Component("desencrypt")
    public class JasyptAlgorithmConfig implements StringEncryptor {
    
        @Override
        public String encrypt(String message) {
            return AESUtils.encrypt(message,AESUtils.getKey());
        }
    
        @Override
        public String decrypt(String encryptedMessage) {
            return AESUtils.decrypt(encryptedMessage,AESUtils.getKey());
        }
    
  • 新增加密工具类:

    public class AESUtils {
      private static final String USER_PWD_KEY = "A39DSSSDFGS4OaHr";
    
      private static final Charset CHARSET = Charset.forName("UTF-8");
    
      // 加密算法名称
      private static final String AES = "AES";
    
      // 偏移量-AES 128位数据块对应偏移量为16位字符串
      private static final String IV = "70w5zbOds3DSFA5C";
    
      // AES-加密方式, CBC-工作模式,PKCS5Padding-填充模式
      private static final String AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5Padding";
    
        /**
         * AES 加密操作
         *
         * @param content 待加密内容
         * @param key     加密密钥
         * @return 返回Base64转码后的加密数据
         */
        public static String encrypt(String content, String key) {
            if (StringUtils.isEmpty(content)) {
                return content;
            }
            try {
                /*
                 * 新建一个密码编译器的实例,由三部分构成,用"/"分隔,分别代表如下
                 * 1. 加密的类型(如AES,DES,RC2等)
                 * 2. 模式(AES中包含ECB,CBC,CFB,CTR,CTS等)
                 * 3. 补码方式(包含nopadding/PKCS5Padding等等)
                 * 依据这三个参数可以创建很多种加密方式
                 */
                Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5PADDING);
                //偏移量
                IvParameterSpec zeroIv = new IvParameterSpec(IV.getBytes(CHARSET));
                byte[] byteContent = content.getBytes(CHARSET);
                //使用加密秘钥
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CHARSET), AES);
                //SecretKeySpec skeySpec = getSecretKey(key);
                cipher.init(Cipher.ENCRYPT_MODE, skeySpec, zeroIv); // 初始化为加密模式的密码器
                byte[] result = cipher.doFinal(byteContent); // 加密
                return Base64.encodeBase64String(result); //通过Base64转码返回
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
    
        /**
         * AES 解密操作
         *
         * @param content
         * @param key
         * @return
         */
        public static String decrypt(String content, String key) {
            if (StringUtils.isEmpty(content)) {
                return content;
            }
            try {
                Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5PADDING);
                IvParameterSpec zeroIv = new IvParameterSpec(IV.getBytes(CHARSET));
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CHARSET), AES);
                //SecretKeySpec skeySpec = getSecretKey(key);
                cipher.init(Cipher.DECRYPT_MODE, skeySpec, zeroIv);
                byte[] result = cipher.doFinal(Base64.decodeBase64(content));
                return new String(result, CHARSET);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
    
        public static String getKey(){
            return USER_PWD_KEY;
        }
    }
    
前后但接口以及数据库存储数据加密:
  • 新增三个注解

    • @EncryptField

      /**
       *  安全字段注解
       * 加在需要加密/解密的属性/参数上
       * 实现自动加密解密
       */
      @Target({ElementType.FIELD,ElementType.PARAMETER})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface EncryptField {
          String[] value() default "";
      }
      
    • @NeedDecrypt

      /**
       *  安全字段注解
       * 加在需要解密的方法参数上
       * 实现自动解密
       */
      @Target({ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface NeedDecrypt {
      }
      
    • @NeedEncrypt

      /**
       *  安全字段注解
       * 加在需要加密的方法参数上
       * 实现自动加密
       */
      @Target({ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface NeedEncrypt {
      }
      
  • 新增两个切面类

    • EncryptAspect

      /**
       * @author 17540
       * 对加了@NeedEncrypt注释的方法的参数进行扫描,参数中存在@EncryptFild修饰的加密字段,则进行加密
       * 当前只适配非嵌套对象参数,List参数,普通String参数
       */
      @Slf4j
      @Aspect
      @Component
      public class EncryptAspect {
      
      
          @Pointcut("@annotation(com.example.encryption.common.anno.NeedEncrypt)")
          public void pointCut() {
          }
      
          @Around("pointCut()")
          public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
              //加密
              return joinPoint.proceed(encrypt(joinPoint));
          }
      
          public Object[] encrypt(ProceedingJoinPoint joinPoint)  {
              Object[] objects=null;
              try {
                  objects = joinPoint.getArgs();
                  if (objects.length != 0) {
                      for (int i = 0; i < objects.length; i++) {
                          //抛砖引玉 ,可自行扩展其他类型字段的判断
                          if (objects[i] instanceof String) {
                              String value = encryptValue(objects[i]);
                              objects[i] = value;
                          } else {
                              encryptData(objects[i]);
                          }
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
              return objects;
          }
      
          private void encryptData(Object obj) throws IllegalAccessException {
              if (Objects.isNull(obj)) {
                  return;
              }
              if (obj instanceof ArrayList) {
                  encryptList(obj);
              } else {
                  encryptObj(obj);
              }
          }
      
          /**
           * 加密对象
           * @param obj
           * @throws IllegalAccessException
           */
          private void encryptObj(Object obj) throws IllegalAccessException {
              if (Objects.isNull(obj)) {
                  log.info("当前需要加密的object为null");
                  return;
              }
              Field[] fields = obj.getClass().getDeclaredFields();
              for (Field field : fields) {
                  boolean containEncryptField = field.isAnnotationPresent(EncryptField.class);
                  if (containEncryptField) {
                      //获取访问权
                      field.setAccessible(true);
                      if (field.get(obj) == null) {
                          continue;
                      }
                      String value = AESUtils.encrypt(String.valueOf(field.get(obj)), AESUtils.getKey());
                      field.set(obj, value);
                  }
              }
          }
      
          /**
           * 针对list<实体来> 进行反射、解密
           * @param obj
           * @throws IllegalAccessException
           */
          private void encryptList(Object obj) throws IllegalAccessException {
              List<Object> result = new ArrayList<>();
              if (obj instanceof ArrayList) {
                  result.addAll((List<?>) obj);
              }
              for (Object object : result) {
                  encryptObj(object);
              }
              obj = result;
          }
      
          /**
           * 加密单个值
           * @param realValue
           * @return
           */
          public String encryptValue(Object realValue) {
              if (Objects.isNull(realValue)) {
                  return null;
              }
              try {
                  realValue = AESUtils.encrypt(String.valueOf(realValue), AESUtils.getKey());
              } catch (Exception e) {
                  log.info("加密异常={}",e.getMessage());
              }
              return String.valueOf(realValue);
          }
      }
      
    • DecryptAspect

      /**
       * @author 17540
       * 对加了@NeedEncrypt注释的方法的参数进行扫描,参数中存在@EncryptFild修饰的加密字段,则进行加密
       * 当前只适配非嵌套对象参数,List参数,普通String参数
       */
      @Slf4j
      @Aspect
      @Component
      public class DecryptAspect {
      
          @Pointcut("@annotation(com.example.encryption.common.anno.NeedDecrypt)")
          public void pointCut() {
          }
      
          @Around("pointCut()")
          public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
              //解密
              Object result = decrypt(joinPoint);
              return result;
          }
      
          public Object decrypt(ProceedingJoinPoint joinPoint) {
              Object result = null;
              try {
                  Object obj = joinPoint.proceed();
                  if (obj != null) {
                      //抛砖引玉 ,可自行扩展其他类型字段的判断
                      if (obj instanceof String) {
                          result = decryptValue(obj);
                      } else {
                          result = decryptData(obj);
                      }
                  }
              } catch (Throwable e) {
                  e.printStackTrace();
              }
              return result;
          }
      
          private Object decryptData(Object obj) throws IllegalAccessException {
      
              if (Objects.isNull(obj)) {
                  return null;
              }
              if (obj instanceof ArrayList) {
                  decryptList(obj);
              } else {
                  decryptObj(obj);
              }
      
      
              return obj;
          }
      
          /**
           * 针对单个实体类进行 解密
           * @param obj
           * @throws IllegalAccessException
           */
          private void decryptObj(Object obj) throws IllegalAccessException {
              Field[] fields = obj.getClass().getDeclaredFields();
              for (Field field : fields) {
                  boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
                  if (hasSecureField) {
                      field.setAccessible(true);
                      String realValue = (String) field.get(obj);
                      if (Objects.isNull(realValue)) {
                          continue;
                      }
                      try{
                          String value = AESUtils.decrypt(realValue, AESUtils.getKey());
                          field.set(obj, value);
                          log.info("解密后={}", value);
                      }catch (Exception e){
                          log.error("解密{}异常=,{}",realValue, e.getMessage());
                      }
                  }
              }
          }
      
          /**
           * 针对list<实体来> 进行反射、解密
           * @param obj
           * @throws IllegalAccessException
           */
          private void decryptList(Object obj) throws IllegalAccessException {
              List<Object> result = new ArrayList<>();
              if (obj instanceof ArrayList) {
                  result.addAll((List<?>) obj);
              }
              for (Object object : result) {
                  decryptObj(object);
              }
              obj = result;
          }
      
      
          public String decryptValue(Object realValue) {
              try {
                  realValue = AESUtils.decrypt(String.valueOf(realValue), AESUtils.getKey());
              } catch (Exception e) {
                  log.info("解密{}异常={}",realValue, e.getMessage());
              }
              return String.valueOf(realValue);
          }
      }
      

你可能感兴趣的:(spring,boot,后端,java,数据脱敏,jasypt,AES)