From 2be6e8845dbdd1a18e20c264b19ebe894b929d39 Mon Sep 17 00:00:00 2001
From: zhangdaiscott <zhangdaiscott@163.com>
Date: Tue, 19 Jul 2022 19:07:28 +0800
Subject: [PATCH] 数据脱敏注解

---
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java |  20 ++++++++++++++++++++
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java |  20 ++++++++++++++++++++
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java  |  21 +++++++++++++++++++++
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java |  81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java        |  55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java     | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 559 insertions(+), 0 deletions(-)
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java
 create mode 100644 jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java

diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java
new file mode 100644
index 0000000..698ecba
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java
@@ -0,0 +1,20 @@
+package org.jeecg.common.desensitization.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 解密注解
+ *
+ * 在方法上定义 将方法返回对象中的敏感字段 解密,需要注意的是,如果没有加密过,解密会出问题,返回原字符串
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface SensitiveDecode {
+
+    /**
+     * 指明需要脱敏的实体类class
+     * @return
+     */
+    Class entity() default Object.class;
+}
diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java
new file mode 100644
index 0000000..eb89d75
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java
@@ -0,0 +1,20 @@
+package org.jeecg.common.desensitization.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 加密注解
+ *
+ * 在方法上声明 将方法返回对象中的敏感字段 加密/格式化
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface SensitiveEncode {
+
+    /**
+     * 指明需要脱敏的实体类class
+     * @return
+     */
+    Class entity() default Object.class;
+}
diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java
new file mode 100644
index 0000000..a887e5a
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java
@@ -0,0 +1,21 @@
+package org.jeecg.common.desensitization.annotation;
+
+
+import org.jeecg.common.desensitization.enums.SensitiveEnum;
+
+import java.lang.annotation.*;
+
+/**
+ * 在字段上定义 标识字段存储的信息是敏感的
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface SensitiveField {
+
+    /**
+     * 不同类型处理不同
+     * @return
+     */
+    SensitiveEnum type() default SensitiveEnum.ENCODE;
+}
diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java
new file mode 100644
index 0000000..da69702
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java
@@ -0,0 +1,81 @@
+package org.jeecg.common.desensitization.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+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.aspectj.lang.reflect.MethodSignature;
+import org.jeecg.common.desensitization.annotation.SensitiveDecode;
+import org.jeecg.common.desensitization.annotation.SensitiveEncode;
+import org.jeecg.common.desensitization.util.SensitiveInfoUtil;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * 敏感数据切面处理类
+ * @Author taoYan
+ * @Date 2022/4/20 17:45
+ **/
+@Slf4j
+@Aspect
+@Component
+public class SensitiveDataAspect {
+
+    /**
+     * 定义切点Pointcut
+     */
+    @Pointcut("@annotation(org.jeecg.common.desensitization.annotation.SensitiveEncode) || @annotation(org.jeecg.common.desensitization.annotation.SensitiveDecode)")
+    public void sensitivePointCut() {
+    }
+
+    @Around("sensitivePointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable {
+        // 处理结果
+        Object result = point.proceed();
+        if(result == null){
+            return result;
+        }
+        Class resultClass = result.getClass();
+        log.debug(" resultClass  = {}" , resultClass);
+
+        if(resultClass.isPrimitive()){
+            //是基本类型 直接返回 不需要处理
+            return result;
+        }
+        // 获取方法注解信息:是哪个实体、是加密还是解密
+        boolean isEncode = true;
+        Class entity = null;
+        MethodSignature methodSignature = (MethodSignature) point.getSignature();
+        Method method = methodSignature.getMethod();
+        SensitiveEncode encode = method.getAnnotation(SensitiveEncode.class);
+        if(encode==null){
+            SensitiveDecode decode = method.getAnnotation(SensitiveDecode.class);
+            if(decode!=null){
+                entity = decode.entity();
+                isEncode = false;
+            }
+        }else{
+            entity = encode.entity();
+        }
+
+        long startTime=System.currentTimeMillis();
+        if(resultClass.equals(entity) || entity.equals(Object.class)){
+            // 方法返回实体和注解的entity一样,如果注解没有申明entity属性则认为是(方法返回实体和注解的entity一样)
+            SensitiveInfoUtil.handlerObject(result, isEncode);
+        } else if(result instanceof List){
+            // 方法返回List<实体>
+            SensitiveInfoUtil.handleList(result, entity, isEncode);
+        }else{
+            // 方法返回一个对象
+            SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
+        }
+        long endTime=System.currentTimeMillis();
+        log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
+
+        return result;
+    }
+
+}
diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java
new file mode 100644
index 0000000..bdffc75
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java
@@ -0,0 +1,55 @@
+package org.jeecg.common.desensitization.enums;
+
+/**
+ * 敏感字段信息类型
+ */
+public enum SensitiveEnum {
+
+
+    /**
+     * 加密
+     */
+    ENCODE,
+
+    /**
+     * 中文名
+     */
+    CHINESE_NAME,
+
+    /**
+     * 身份证号
+     */
+    ID_CARD,
+
+    /**
+     * 座机号
+     */
+    FIXED_PHONE,
+
+    /**
+     * 手机号
+     */
+    MOBILE_PHONE,
+
+    /**
+     * 地址
+     */
+    ADDRESS,
+
+    /**
+     * 电子邮件
+     */
+    EMAIL,
+
+    /**
+     * 银行卡
+     */
+    BANK_CARD,
+
+    /**
+     * 公司开户银行联号
+     */
+    CNAPS_CODE;
+
+
+}
diff --git a/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java
new file mode 100644
index 0000000..e872437
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java
@@ -0,0 +1,362 @@
+package org.jeecg.common.desensitization.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.desensitization.annotation.SensitiveField;
+import org.jeecg.common.desensitization.enums.SensitiveEnum;
+import org.jeecg.common.util.encryption.AesEncryptUtil;
+import org.jeecg.common.util.oConvertUtils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 敏感信息处理工具类
+ * @author taoYan
+ * @date 2022/4/20 18:01
+ **/
+@Slf4j
+public class SensitiveInfoUtil {
+
+    /**
+     * 处理嵌套对象
+     * @param obj 方法返回值
+     * @param entity 实体class
+     * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
+     * @throws IllegalAccessException
+     */
+    public static void handleNestedObject(Object obj, Class entity, boolean isEncode) throws IllegalAccessException {
+        Field[] fields = obj.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            if(field.getType().isPrimitive()){
+                continue;
+            }
+            if(field.getType().equals(entity)){
+                // 对象里面是实体
+                field.setAccessible(true);
+                Object nestedObject = field.get(obj);
+                handlerObject(nestedObject, isEncode);
+                break;
+            }else{
+                // 对象里面是List<实体>
+                if(field.getGenericType() instanceof ParameterizedType){
+                    ParameterizedType pt = (ParameterizedType)field.getGenericType();
+                    if(pt.getRawType().equals(List.class)){
+                        if(pt.getActualTypeArguments()[0].equals(entity)){
+                            field.setAccessible(true);
+                            Object nestedObject = field.get(obj);
+                            handleList(nestedObject, entity, isEncode);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 处理Object
+     * @param obj 方法返回值
+     * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
+     * @return
+     * @throws IllegalAccessException
+     */
+    public static Object handlerObject(Object obj, boolean isEncode) throws IllegalAccessException {
+        log.debug(" obj --> "+ obj.toString());
+        long startTime=System.currentTimeMillis();
+        if (oConvertUtils.isEmpty(obj)) {
+            return obj;
+        }
+        // 判断是不是一个对象
+        Field[] fields = obj.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            boolean isSensitiveField = field.isAnnotationPresent(SensitiveField.class);
+            if(isSensitiveField){
+                // 必须有SensitiveField注解 才作处理
+                if(field.getType().isAssignableFrom(String.class)){
+                    //必须是字符串类型 才作处理
+                    field.setAccessible(true);
+                    String realValue = (String) field.get(obj);
+                    if(realValue==null || "".equals(realValue)){
+                        continue;
+                    }
+                    SensitiveField sf = field.getAnnotation(SensitiveField.class);
+                    if(isEncode==true){
+                        //加密
+                        String value = SensitiveInfoUtil.getEncodeData(realValue,  sf.type());
+                        field.set(obj, value);
+                    }else{
+                        //解密只处理 encode类型的
+                        if(sf.type().equals(SensitiveEnum.ENCODE)){
+                            String value = SensitiveInfoUtil.getDecodeData(realValue);
+                            field.set(obj, value);
+                        }
+                    }
+                }
+            }
+        }
+        //long endTime=System.currentTimeMillis();
+        //log.info((isEncode ? "加密操作," : "解密操作,") + "当前程序耗时:" + (endTime - startTime) + "ms");
+        return obj;
+    }
+
+    /**
+     * 处理 List<实体>
+     * @param obj
+     * @param entity
+     * @param isEncode(true: 加密操作 / false:解密操作)
+     */
+    public static void handleList(Object obj, Class entity, boolean isEncode){
+        List list = (List)obj;
+        if(list.size()>0){
+            Object first = list.get(0);
+            if(first.getClass().equals(entity)){
+                for(int i=0; i<list.size(); i++){
+                    Object temp = list.get(i);
+                    try {
+                        handlerObject(temp, isEncode);
+                    } catch (IllegalAccessException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 处理数据 获取解密后的数据
+     * @param data
+     * @return
+     */
+    public static String getDecodeData(String data){
+        String result = null;
+        try {
+            result = AesEncryptUtil.desEncrypt(data);
+        } catch (Exception exception) {
+            log.warn("数据解密错误,原数据:"+data);
+        }
+        //解决debug模式下,加解密失效导致中文被解密变成空的问题
+        if(oConvertUtils.isEmpty(result) && oConvertUtils.isNotEmpty(data)){
+            result = data;
+        }
+        return result;
+    }
+
+    /**
+     * 处理数据 获取加密后的数据 或是格式化后的数据
+     * @param data 字符串
+     * @param sensitiveEnum 类型
+     * @return 处理后的字符串
+     */
+    public static String getEncodeData(String data, SensitiveEnum sensitiveEnum){
+        String result;
+        switch (sensitiveEnum){
+            case ENCODE:
+                try {
+                    result = AesEncryptUtil.encrypt(data);
+                } catch (Exception exception) {
+                    log.error("数据加密错误", exception.getMessage());
+                    result = data;
+                }
+                break;
+            case CHINESE_NAME:
+                result = chineseName(data);
+                break;
+            case ID_CARD:
+                result = idCardNum(data);
+                break;
+            case FIXED_PHONE:
+                result = fixedPhone(data);
+                break;
+            case MOBILE_PHONE:
+                result = mobilePhone(data);
+                break;
+            case ADDRESS:
+                result = address(data, 3);
+                break;
+            case EMAIL:
+                result = email(data);
+                break;
+            case BANK_CARD:
+                result = bankCard(data);
+                break;
+            case CNAPS_CODE:
+                result = cnapsCode(data);
+                break;
+            default:
+                result = data;
+        }
+        return result;
+    }
+
+
+    /**
+     * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
+     * @param fullName 全名
+     * @return <例子:李**>
+     */
+    private static String chineseName(String fullName) {
+        if (oConvertUtils.isEmpty(fullName)) {
+            return "";
+        }
+        return formatRight(fullName, 1);
+    }
+
+    /**
+     * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
+     * @param familyName 姓
+     * @param firstName 名
+     * @return <例子:李**>
+     */
+    private static String chineseName(String familyName, String firstName) {
+        if (oConvertUtils.isEmpty(familyName) || oConvertUtils.isEmpty(firstName)) {
+            return "";
+        }
+        return chineseName(familyName + firstName);
+    }
+
+    /**
+     * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。
+     * @param id 身份证号
+     * @return <例子:*************5762>
+     */
+    private static String idCardNum(String id) {
+        if (oConvertUtils.isEmpty(id)) {
+            return "";
+        }
+        return formatLeft(id, 4);
+
+    }
+
+    /**
+     * [固定电话] 后四位,其他隐藏
+     * @param num 固定电话
+     * @return <例子:****1234>
+     */
+    private static String fixedPhone(String num) {
+        if (oConvertUtils.isEmpty(num)) {
+            return "";
+        }
+        return formatLeft(num, 4);
+    }
+
+    /**
+     * [手机号码] 前三位,后四位,其他隐藏
+     * @param num 手机号码
+     * @return <例子:138******1234>
+     */
+    private static String mobilePhone(String num) {
+        if (oConvertUtils.isEmpty(num)) {
+            return "";
+        }
+        int len = num.length();
+        if(len<11){
+            return num;
+        }
+        return formatBetween(num, 3, 4);
+    }
+
+    /**
+     * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护
+     * @param address 地址
+     * @param sensitiveSize 敏感信息长度
+     * @return <例子:北京市海淀区****>
+     */
+    private static String address(String address, int sensitiveSize) {
+        if (oConvertUtils.isEmpty(address)) {
+            return "";
+        }
+        int len = address.length();
+        if(len<sensitiveSize){
+            return address;
+        }
+        return formatRight(address, sensitiveSize);
+    }
+
+    /**
+     * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示
+     * @param email 电子邮箱
+     * @return <例子:g**@163.com>
+     */
+    private static String email(String email) {
+        if (oConvertUtils.isEmpty(email)) {
+            return "";
+        }
+        int index = email.indexOf("@");
+        if (index <= 1){
+            return email;
+        }
+        String begin = email.substring(0, 1);
+        String end = email.substring(index);
+        String stars = "**";
+        return begin + stars + end;
+    }
+
+    /**
+     * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号
+     * @param cardNum 银行卡号
+     * @return <例子:6222600**********1234>
+     */
+    private static String bankCard(String cardNum) {
+        if (oConvertUtils.isEmpty(cardNum)) {
+            return "";
+        }
+        return formatBetween(cardNum, 6, 4);
+    }
+
+    /**
+     * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号
+     * @param code 公司开户银行联号
+     * @return <例子:12********>
+     */
+    private static String cnapsCode(String code) {
+        if (oConvertUtils.isEmpty(code)) {
+            return "";
+        }
+        return formatRight(code, 2);
+    }
+
+
+    /**
+     * 将右边的格式化成*
+     * @param str 字符串
+     * @param reservedLength 保留长度
+     * @return 格式化后的字符串
+     */
+    private static String formatRight(String str, int reservedLength){
+        String name = str.substring(0, reservedLength);
+        String stars = String.join("", Collections.nCopies(str.length()-reservedLength, "*"));
+        return name + stars;
+    }
+
+    /**
+     * 将左边的格式化成*
+     * @param str 字符串
+     * @param reservedLength 保留长度
+     * @return 格式化后的字符串
+     */
+    private static String formatLeft(String str, int reservedLength){
+        int len = str.length();
+        String show = str.substring(len-reservedLength);
+        String stars = String.join("", Collections.nCopies(len-reservedLength, "*"));
+        return stars + show;
+    }
+
+    /**
+     * 将中间的格式化成*
+     * @param str 字符串
+     * @param beginLen 开始保留长度
+     * @param endLen 结尾保留长度
+     * @return 格式化后的字符串
+     */
+    private static String formatBetween(String str, int beginLen, int endLen){
+        int len = str.length();
+        String begin = str.substring(0, beginLen);
+        String end = str.substring(len-endLen);
+        String stars = String.join("", Collections.nCopies(len-beginLen-endLen, "*"));
+        return begin + stars + end;
+    }
+
+}
--
libgit2 0.22.2