spring自定义注解实现数据加解密和脱敏

1,462 阅读8分钟

前言

大家好,在日常开发中涉及到一些比较敏感的数据。比如身份证,手机号等这些数据我们不能直接保存到数据库中,而是需要通过数据加密的方式,采用AES,DES等加密方式,加密之后在保存到数据库中,而在查询时,再通过解密的方式将数据返回给前端。如果数据库敏感数据不加密,那么数据库万一被他人盗取后果不堪设想。

参考文档

  1. springBoot中如何给敏感数据脱敏
  2. Springboot AOP对指定敏感字段数据加密存储的实现

思路整理

  1. 要实现这一功能肯定时需要使用注解的方式拦截请求方法中的数据进行处理的

  2. 说明:

    1. 接口请求端分为两种请求方式去1. 请求查询用户数据,2. 请求保存用户数据
    2. 请求保存用户数据时,我们直接判断需要加密的字段,然后保存到数据库即可
    3. 请求查询用户数据时,我们把数据库的数据解密,然后判断前端是否需要脱敏处理,将数据返回给前端即可

  1. 其实使用的原理也就是spring中AOP,大家可以详细了解一下,接下来话不多说看看我是如何实现的吧

具体实现

特别说明

  1. 实现此功能的过程中会引入相应的jar

1. 声明数据处理的类型

/**判断是否加密,解密或者不处理数据
 * @author xiaYZ  2022/9/3
 * @version: 1.0
 */
public enum DataHandle {
​
​
​
    /**
     * 加密数据
     */
    ENCRYPT,
​
    /**
     * 解密数据
     */
    DECRYPT,
​
    /**
     * 数据不做加解密处理直接通过,然后进行脱敏处理
     */
    PASS_ADN_SENSITIVE,
​
}

2. 声明加密的类型

/**加密解密的枚举类
 * @author xiaYZ  2022/9/3
 * @version: 1.0
 */
public enum EncryptWayEnum {
​
    /**
     * AES加密
     */
    AES,
​
    /**
     * DES加密
     */
    DES,
​
}
​

3. 声明脱敏的数据类型

@Getter
public enum PrivacyTypeEnum {
​
        /** 自定义(此项需设置脱敏的范围)*/
        CUSTOMER,
​
        /** 姓名 */
        NAME,
​
        /** 身份证号 */
        ID_CARD,
​
        /** 手机号 */
        PHONE,
​
        /** 邮箱 */
        EMAIL,
    }

4. 声明数据加解密注解

/**
 * description: 方法层面声明加解密参数,数据加密解密注解 <br>
 * date: 2022/9/3 14:31 <br>
 * author: 10412 <br>
 * version: 1.0 <br>
 * @author 10412
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataProcess {
​
​
    /**
     * 处理数据方式 加密,解密,通过,默认
     */
    DataHandle sign() default DataHandle.PASS_ADN_SENSITIVE;
​
}

5. 声明实体类中数据加解密方式

/**
 * description: EncryptField2 <br>
 * date: 2022/9/3 15:33 <br>
 * author: 10412 <br>
 * version: 1.0 <br>
 * @author 10412
 */
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldProcess {
​
    /**
     * 加密方式 默认AES
     */
    EncryptWayEnum enumType() default EncryptWayEnum.AES;
​
    /**
     * 加密秘钥默认 9B4E7C1045A02A2F 秘钥默认16个字符
     */
    String key() default "9B4E7C1045A02A2F";
    /**
     * 是否需要脱敏处理
     */
    boolean hasSensitive() default false;
    /**
     * 脱敏数据类型(默认自定义)
     */
    PrivacyTypeEnum privacyEnum() default PrivacyTypeEnum.ID_CARD;
​
    /**
     * 前置不需要打码的长度
     */
    int prefixNoMaskLen() default 1;
​
    /**
     * 后置不需要打码的长度
     */
    int suffixNoMaskLen() default 1;
​
    /**
     * 用什么打码
     */
    String symbol() default "*";
}

6. 数据脱敏工具类

/**
 * @description:
 * @author: xiaYZ
 * @createDate: 2022/6/21
 */
public class PrivacyUtil {
​
​
​
    /**
     * 隐藏邮箱
     */
    public static String hideEmail(String email) {
        return email.replaceAll("(\w?)(\w+)(\w)(@\w+\.[a-z]+(\.[a-z]+)?)", "$1****$3$4");
    }
​
​
    /**
     * 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任**
     */
    public static String hideChineseName(String chineseName) {
        if (chineseName == null) {
            return null;
        }
        return desValue(chineseName, 1, 0, "*");
    }
​
   /**
    * 【身份证号】显示前4位, 后2位
    */
   public static String hideIdCard(String idCard) {
       return desValue(idCard, 4, 2, "*");
   }
​
   /**
    * 【手机号码】前三位,后四位,其他隐藏。
    */
   public static String hidePhone(String phone) {
       return desValue(phone, 3, 4, "*");
   }
​
    /**
     * 对字符串进行脱敏操作
     * @param origin          原始字符串
     * @param prefixNoMaskLen 左侧需要保留几位明文字段
     * @param suffixNoMaskLen 右侧需要保留几位明文字段
     * @param maskStr         用于遮罩的字符串, 如'*'
     * @return 脱敏后结果
     */
    public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
        if (origin == null) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0, n = origin.length(); i < n; i++) {
            if (i < prefixNoMaskLen) {
                sb.append(origin.charAt(i));
                continue;
            }
            if (i > (n - suffixNoMaskLen - 1)) {
                sb.append(origin.charAt(i));
                continue;
            }
            sb.append(maskStr);
        }
        return sb.toString();
    }
​
}
​

7.处理注解的方法

/** 数据加密解密,脱敏操作
 * @author xiaYZ 2022/9/3
 * @version: 1.0
 */
@Slf4j
@Aspect
@Component
public class DataProcessAspect {
​
        //声明注解所处的位置
    @Pointcut("@annotation(com.example.springbootmybatis.rule2.DataProcess)")
    public void pointCut() {
​
    }
​
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 通过反射获得原始方法信息
        MethodSignature signature  =  (MethodSignature)joinPoint.getSignature();
        //获取方法注解DateProcess中参数
        DataProcess dateProcess = signature.getMethod().getAnnotation(DataProcess.class);
​
        if(dateProcess.sign() == DataHandle.ENCRYPT){
            //加密方法参数
            encrypt(joinPoint);
        }else if(dateProcess.sign() == DataHandle.DECRYPT){
            //判断是否为解密操作,进行解密,方法返回解密之后的数据
           return decrypt(joinPoint);
        }else if(dateProcess.sign() == DataHandle.PASS_ADN_SENSITIVE){
          return desensitization(joinPoint);
        }
        //其他情况直接返回原数据
        return joinPoint.proceed();
    }
​
    public void encrypt(ProceedingJoinPoint joinPoint)  {
        Object[] params = null;
        try {
            //获取方法的参数列表,方法参数有多个数据arg1,arg2等
            params = joinPoint.getArgs();
            Object target = joinPoint.getTarget();
            System.out.println(target);
            if (params.length != 0) {
​
                for (int i = 0; i < params.length; i++) {
                    //判断此方法参数是否为List数组类型
                    if(params[i] instanceof List){
                        //加密对象为数组时,构建数组对象
                        List<Object> result = new ArrayList<>((List<?>) params[i]);
                        for (Object object : result) {
                            //数组对象循环加密
                            encryptObject(object);
                        }
                    }else{
                        //对象类加密
                        encryptObject(params[i]);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 加密对象
     * @param obj
     * @throws IllegalAccessException
     */
    private void encryptObject(Object obj) throws Exception {
​
        if (Objects.isNull(obj)) {
            log.info("当前需要加密的object为null");
            return;
        }
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            //类的每个字段判断是否需要加密
            FieldProcess encryptField = field.getAnnotation(FieldProcess.class);
            if (Objects.nonNull(encryptField)) {
                //获取访问权
                field.setAccessible(true);
                String encryptCode = String.valueOf(field.get(obj));
                //加密秘钥
                String key = encryptField.key();
                if(encryptField.enumType() == EncryptWayEnum.AES){
                    AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
                    // AES 加密
                    encryptCode = aes.encryptHex(String.valueOf(field.get(obj)));
                }else if(encryptField.enumType() == EncryptWayEnum.DES){
                    DES des = SecureUtil.des(key.getBytes(StandardCharsets.UTF_8));
​
                    encryptCode = des.encryptHex(String.valueOf(field.get(obj)));
                }else if(encryptField.enumType() == EncryptWayEnum.SHA1){
                    // TODO SHA1 加密
​
                }
                field.set(obj, encryptCode);
            }
        }
    }
​
​
​
​
    /***
     * description:解密操作
     * @author xiaYZ
     * create time:
      * @param joinPoint 解密的节点
     */
    public Object decrypt(ProceedingJoinPoint joinPoint) {
        Object returnObject = null;
        try {
            //获取方法返回的对象
             returnObject = joinPoint.proceed();
            if (returnObject != null) {
                //解密对象
                if (returnObject instanceof ArrayList) {
                    //构建对象数组,并解密,针对list<实体来> 进行反射、解密
                    List<Object> resultList = new ArrayList<>((List<?>) returnObject);
                    for (Object object : resultList) {
                        decryptObj(object);
                    }
                } else {
                    decryptObj(returnObject);
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return returnObject;
    }
​
​
    /**
     * 针对单个实体类进行 解密
     * @param obj
     * @throws IllegalAccessException
     */
    private void decryptObj(Object obj) throws IOException, IllegalAccessException {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            //类的每个字段判断是否需要加密
            FieldProcess encryptField = field.getAnnotation(FieldProcess.class);
            if (Objects.nonNull(encryptField)) {
                //获取访问权
                field.setAccessible(true);
                String decryptCode = String.valueOf(field.get(obj));
                //加密秘钥
                String key = encryptField.key();
                if(encryptField.enumType() == EncryptWayEnum.AES){
                    // 构建
                    AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
                    // 解密
                    decryptCode = aes.decryptStr(String.valueOf(field.get(obj)));
                }else if(encryptField.enumType() == EncryptWayEnum.DES){
                    DES des = SecureUtil.des(key.getBytes(StandardCharsets.UTF_8));
                    decryptCode = des.decryptStr(String.valueOf(field.get(obj)));
                }else if(encryptField.enumType() == EncryptWayEnum.SHA1){
                    // TODO SHA1 解密
​
                }
                //判断解密是否需要脱敏处理
                if(encryptField.hasSensitive()){
                    decryptCode = desensitizationWay(decryptCode, encryptField);
                }
                field.set(obj,decryptCode);
            }
        }
    }
​
​
​
    /***
     * description 方法返回数据直接脱敏
     * version 1.0
     * @date 2022/9/10 14:42
     * @author xiaYZ
     * @param joinPoint
     * @return 
     */
    public Object desensitization(ProceedingJoinPoint joinPoint){
        Object returnObject = null;
        try {
            //获取方法返回的对象
            returnObject = joinPoint.proceed();
            if (returnObject != null) {
                //判断脱敏的数据是否为List数组
                if (returnObject instanceof ArrayList) {
                    //构建对象数组,数组数据脱敏
                    List<Object> resultList = new ArrayList<>((List<?>) returnObject);
                    for (Object object : resultList) {
                        desensitizationObject(object);
                    }
                } else {
                    //对象类数据脱敏
                    desensitizationObject(returnObject);
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return returnObject;
    }
​
    /***
     * description 数据对象脱敏
     * version 1.0
     * @date 2022/9/10 14:43
     * @author xiaYZ
     * @param obj
     * @return 
     */
    public void desensitizationObject(Object obj) throws IOException, IllegalAccessException {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            //类的每个字段判断是否需要加密
            FieldProcess encryptField = field.getAnnotation(FieldProcess.class);
            if (Objects.nonNull(encryptField)) {
                //获取访问权
                field.setAccessible(true);
                String encode = String.valueOf(field.get(obj));
                String desensitizeCode = desensitizationWay(encode, encryptField);
                field.set(obj,desensitizeCode);
            }
        }
    }
​
​
​
    /***
     * description:脱敏操作
     * @author xiaYZ
     * create time:
      * @param code 脱敏前字符串
     * @param encryptField 脱敏规则
     * @return java.lang.String
     */
    public String desensitizationWay(String code, FieldProcess encryptField) throws IOException {
        if(encryptField.privacyEnum() == PrivacyTypeEnum.CUSTOMER){
            return  PrivacyUtil.desValue(code, encryptField.prefixNoMaskLen(), encryptField.suffixNoMaskLen(), encryptField.symbol());
        }else if(encryptField.privacyEnum() ==  PrivacyTypeEnum.NAME) {
            return   PrivacyUtil.hideChineseName(code);
        } else if(encryptField.privacyEnum() == PrivacyTypeEnum.ID_CARD) {
            return  PrivacyUtil.hideIdCard(code);
        }else if(encryptField.privacyEnum() == PrivacyTypeEnum.PHONE) {
            return  PrivacyUtil.hidePhone(code);
        }else if(encryptField.privacyEnum() ==  PrivacyTypeEnum.EMAIL) {
            return  PrivacyUtil.hideEmail(code);
        }else {
           return code;
        }
    }
​
​
}

最终效果

1.数据加密存入数据库

  1. service层处理数据
/**
     * description 批量新增
     * version 1.0
     * @date 2022/9/10 10:35
     * @author xiaYZ
     * @param userList
     * @return 
     */
    @DataProcess(sign = DataHandle.ENCRYPT)
    public void  batchInsert(List<User> userList){
        userDao.batchInsert(userList);
    }
  1. 使用@DataProcess注解声明处理方式为数据加密
  2. 实体类注解使用方式
/**
 * @description:
 * @author: xiaYZ
 * @createDate: 2021/12/28
 * @version: 1.0
 */
@Data
@ExcelTarget("user")
public class User implements Serializable {
​
    /**
     * 主键id
     */
    
    private  Long  id;
​
    /**
     * 用户名
     */
    
    private String userName;
​
    /**
     * 身份证号码,hasSensitive声明解密之后是否需要脱敏,privacyEnum脱敏规则是什么
     */
   
    @FieldProcess(hasSensitive = true,privacyEnum = PrivacyTypeEnum.ID_CARD)
    private String idCardNumber;
​
    /**
     * 手机号码
     */
  
    @FieldProcess(hasSensitive = true,privacyEnum = PrivacyTypeEnum.PHONE)
    private String phoneNumber;
​
    /**
     * 部门id
     */
    private Long deptId;
​
    /**
     * 部门名称
     */
    private String deptName;
​
    /**
     * 部门类
     */
    private Dept dept;
}
​
  1. @FieldProcess注解还可以声明使用的加密方式和密钥key,和脱敏的规则
  2. 如:@FieldProcess(enumType = EncryptWayEnum.DES,key = "1234567891234567")
  3. 效果呈现

调用接口方式 数据库保存数据

2.数据界面并脱敏数据返回给前端

  1. service层注解使用方式
    /**
     * description 查询用户列表数据
     * version 1.0
     * @date 2022/9/10 14:58
     * @author xiaYZ
     * @param userIdList
     * @return 
     */
    @DataProcess(sign = DataHandle.DECRYPT) //解密数据并脱敏
    // @DataProcess //直接脱敏数据
    public List<User> findUserList(List<Long> userIdList){
      return   userDao.findUserList(userIdList);
    }
  1. 如果是需要解密并脱敏则使用 @DataProcess(sign = DataHandle.DECRYPT)注解,如果仅仅只需要数据脱敏则使用 @DataProcess即可
  2. 效果截图,刚刚加密的两个数据解密之后脱敏返回来了

3.数据只需要脱敏

  1. service层注解使用
 /**
     * description 查询用户列表数据
     * version 1.0
     * @date 2022/9/10 14:58
     * @author xiaYZ
     * @param userIdList
     * @return 
     */
    // @DataProcess(sign = DataHandle.DECRYPT) //解密数据并脱敏
    @DataProcess //直接脱敏数据
    public List<User> findUserList(List<Long> userIdList){
      return   userDao.findUserList(userIdList);
    }
  1. 效果截图

查询数据库中未加密的张三、李四两条数据 数据库中未加密处理的两条数据也返回了

总结

  1. 数据加密解密脱敏处理其实就是数据的前置处理和后置处理的问题,从前端获取数据加密存入数据库,和从数据库查询数据解密返回给前端都是一个原理,实际使用的都是Spring中的AOP原理
  2. [项目源码](springboot-mybatis · xiayz/SpringBootAll - 码云 - 开源中国 (gitee.com))