Commit 2be6e8845dbdd1a18e20c264b19ebe894b929d39

Authored by zhangdaiscott
1 parent c057fbcd

数据脱敏注解

jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveDecode.java 0 → 100644
  1 +package org.jeecg.common.desensitization.annotation;
  2 +
  3 +import java.lang.annotation.*;
  4 +
  5 +/**
  6 + * 解密注解
  7 + *
  8 + * 在方法上定义 将方法返回对象中的敏感字段 解密,需要注意的是,如果没有加密过,解密会出问题,返回原字符串
  9 + */
  10 +@Documented
  11 +@Retention(RetentionPolicy.RUNTIME)
  12 +@Target({ElementType.METHOD})
  13 +public @interface SensitiveDecode {
  14 +
  15 + /**
  16 + * 指明需要脱敏的实体类class
  17 + * @return
  18 + */
  19 + Class entity() default Object.class;
  20 +}
... ...
jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveEncode.java 0 → 100644
  1 +package org.jeecg.common.desensitization.annotation;
  2 +
  3 +import java.lang.annotation.*;
  4 +
  5 +/**
  6 + * 加密注解
  7 + *
  8 + * 在方法上声明 将方法返回对象中的敏感字段 加密/格式化
  9 + */
  10 +@Documented
  11 +@Retention(RetentionPolicy.RUNTIME)
  12 +@Target({ElementType.METHOD})
  13 +public @interface SensitiveEncode {
  14 +
  15 + /**
  16 + * 指明需要脱敏的实体类class
  17 + * @return
  18 + */
  19 + Class entity() default Object.class;
  20 +}
... ...
jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/annotation/SensitiveField.java 0 → 100644
  1 +package org.jeecg.common.desensitization.annotation;
  2 +
  3 +
  4 +import org.jeecg.common.desensitization.enums.SensitiveEnum;
  5 +
  6 +import java.lang.annotation.*;
  7 +
  8 +/**
  9 + * 在字段上定义 标识字段存储的信息是敏感的
  10 + */
  11 +@Documented
  12 +@Retention(RetentionPolicy.RUNTIME)
  13 +@Target(ElementType.FIELD)
  14 +public @interface SensitiveField {
  15 +
  16 + /**
  17 + * 不同类型处理不同
  18 + * @return
  19 + */
  20 + SensitiveEnum type() default SensitiveEnum.ENCODE;
  21 +}
... ...
jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/aspect/SensitiveDataAspect.java 0 → 100644
  1 +package org.jeecg.common.desensitization.aspect;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.aspectj.lang.ProceedingJoinPoint;
  5 +import org.aspectj.lang.annotation.Around;
  6 +import org.aspectj.lang.annotation.Aspect;
  7 +import org.aspectj.lang.annotation.Pointcut;
  8 +import org.aspectj.lang.reflect.MethodSignature;
  9 +import org.jeecg.common.desensitization.annotation.SensitiveDecode;
  10 +import org.jeecg.common.desensitization.annotation.SensitiveEncode;
  11 +import org.jeecg.common.desensitization.util.SensitiveInfoUtil;
  12 +import org.springframework.stereotype.Component;
  13 +
  14 +import java.lang.reflect.Method;
  15 +import java.util.List;
  16 +
  17 +/**
  18 + * 敏感数据切面处理类
  19 + * @Author taoYan
  20 + * @Date 2022/4/20 17:45
  21 + **/
  22 +@Slf4j
  23 +@Aspect
  24 +@Component
  25 +public class SensitiveDataAspect {
  26 +
  27 + /**
  28 + * 定义切点Pointcut
  29 + */
  30 + @Pointcut("@annotation(org.jeecg.common.desensitization.annotation.SensitiveEncode) || @annotation(org.jeecg.common.desensitization.annotation.SensitiveDecode)")
  31 + public void sensitivePointCut() {
  32 + }
  33 +
  34 + @Around("sensitivePointCut()")
  35 + public Object around(ProceedingJoinPoint point) throws Throwable {
  36 + // 处理结果
  37 + Object result = point.proceed();
  38 + if(result == null){
  39 + return result;
  40 + }
  41 + Class resultClass = result.getClass();
  42 + log.debug(" resultClass = {}" , resultClass);
  43 +
  44 + if(resultClass.isPrimitive()){
  45 + //是基本类型 直接返回 不需要处理
  46 + return result;
  47 + }
  48 + // 获取方法注解信息:是哪个实体、是加密还是解密
  49 + boolean isEncode = true;
  50 + Class entity = null;
  51 + MethodSignature methodSignature = (MethodSignature) point.getSignature();
  52 + Method method = methodSignature.getMethod();
  53 + SensitiveEncode encode = method.getAnnotation(SensitiveEncode.class);
  54 + if(encode==null){
  55 + SensitiveDecode decode = method.getAnnotation(SensitiveDecode.class);
  56 + if(decode!=null){
  57 + entity = decode.entity();
  58 + isEncode = false;
  59 + }
  60 + }else{
  61 + entity = encode.entity();
  62 + }
  63 +
  64 + long startTime=System.currentTimeMillis();
  65 + if(resultClass.equals(entity) || entity.equals(Object.class)){
  66 + // 方法返回实体和注解的entity一样,如果注解没有申明entity属性则认为是(方法返回实体和注解的entity一样)
  67 + SensitiveInfoUtil.handlerObject(result, isEncode);
  68 + } else if(result instanceof List){
  69 + // 方法返回List<实体>
  70 + SensitiveInfoUtil.handleList(result, entity, isEncode);
  71 + }else{
  72 + // 方法返回一个对象
  73 + SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
  74 + }
  75 + long endTime=System.currentTimeMillis();
  76 + log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
  77 +
  78 + return result;
  79 + }
  80 +
  81 +}
... ...
jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/enums/SensitiveEnum.java 0 → 100644
  1 +package org.jeecg.common.desensitization.enums;
  2 +
  3 +/**
  4 + * 敏感字段信息类型
  5 + */
  6 +public enum SensitiveEnum {
  7 +
  8 +
  9 + /**
  10 + * 加密
  11 + */
  12 + ENCODE,
  13 +
  14 + /**
  15 + * 中文名
  16 + */
  17 + CHINESE_NAME,
  18 +
  19 + /**
  20 + * 身份证号
  21 + */
  22 + ID_CARD,
  23 +
  24 + /**
  25 + * 座机号
  26 + */
  27 + FIXED_PHONE,
  28 +
  29 + /**
  30 + * 手机号
  31 + */
  32 + MOBILE_PHONE,
  33 +
  34 + /**
  35 + * 地址
  36 + */
  37 + ADDRESS,
  38 +
  39 + /**
  40 + * 电子邮件
  41 + */
  42 + EMAIL,
  43 +
  44 + /**
  45 + * 银行卡
  46 + */
  47 + BANK_CARD,
  48 +
  49 + /**
  50 + * 公司开户银行联号
  51 + */
  52 + CNAPS_CODE;
  53 +
  54 +
  55 +}
... ...
jeecg-boot/jeecg-boot-base/jeecg-boot-base-core/src/main/java/org/jeecg/common/desensitization/util/SensitiveInfoUtil.java 0 → 100644
  1 +package org.jeecg.common.desensitization.util;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.jeecg.common.desensitization.annotation.SensitiveField;
  5 +import org.jeecg.common.desensitization.enums.SensitiveEnum;
  6 +import org.jeecg.common.util.encryption.AesEncryptUtil;
  7 +import org.jeecg.common.util.oConvertUtils;
  8 +
  9 +import java.lang.reflect.Field;
  10 +import java.lang.reflect.ParameterizedType;
  11 +import java.util.Collections;
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 敏感信息处理工具类
  16 + * @author taoYan
  17 + * @date 2022/4/20 18:01
  18 + **/
  19 +@Slf4j
  20 +public class SensitiveInfoUtil {
  21 +
  22 + /**
  23 + * 处理嵌套对象
  24 + * @param obj 方法返回值
  25 + * @param entity 实体class
  26 + * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
  27 + * @throws IllegalAccessException
  28 + */
  29 + public static void handleNestedObject(Object obj, Class entity, boolean isEncode) throws IllegalAccessException {
  30 + Field[] fields = obj.getClass().getDeclaredFields();
  31 + for (Field field : fields) {
  32 + if(field.getType().isPrimitive()){
  33 + continue;
  34 + }
  35 + if(field.getType().equals(entity)){
  36 + // 对象里面是实体
  37 + field.setAccessible(true);
  38 + Object nestedObject = field.get(obj);
  39 + handlerObject(nestedObject, isEncode);
  40 + break;
  41 + }else{
  42 + // 对象里面是List<实体>
  43 + if(field.getGenericType() instanceof ParameterizedType){
  44 + ParameterizedType pt = (ParameterizedType)field.getGenericType();
  45 + if(pt.getRawType().equals(List.class)){
  46 + if(pt.getActualTypeArguments()[0].equals(entity)){
  47 + field.setAccessible(true);
  48 + Object nestedObject = field.get(obj);
  49 + handleList(nestedObject, entity, isEncode);
  50 + break;
  51 + }
  52 + }
  53 + }
  54 + }
  55 + }
  56 + }
  57 +
  58 + /**
  59 + * 处理Object
  60 + * @param obj 方法返回值
  61 + * @param isEncode 是否加密(true: 加密操作 / false:解密操作)
  62 + * @return
  63 + * @throws IllegalAccessException
  64 + */
  65 + public static Object handlerObject(Object obj, boolean isEncode) throws IllegalAccessException {
  66 + log.debug(" obj --> "+ obj.toString());
  67 + long startTime=System.currentTimeMillis();
  68 + if (oConvertUtils.isEmpty(obj)) {
  69 + return obj;
  70 + }
  71 + // 判断是不是一个对象
  72 + Field[] fields = obj.getClass().getDeclaredFields();
  73 + for (Field field : fields) {
  74 + boolean isSensitiveField = field.isAnnotationPresent(SensitiveField.class);
  75 + if(isSensitiveField){
  76 + // 必须有SensitiveField注解 才作处理
  77 + if(field.getType().isAssignableFrom(String.class)){
  78 + //必须是字符串类型 才作处理
  79 + field.setAccessible(true);
  80 + String realValue = (String) field.get(obj);
  81 + if(realValue==null || "".equals(realValue)){
  82 + continue;
  83 + }
  84 + SensitiveField sf = field.getAnnotation(SensitiveField.class);
  85 + if(isEncode==true){
  86 + //加密
  87 + String value = SensitiveInfoUtil.getEncodeData(realValue, sf.type());
  88 + field.set(obj, value);
  89 + }else{
  90 + //解密只处理 encode类型的
  91 + if(sf.type().equals(SensitiveEnum.ENCODE)){
  92 + String value = SensitiveInfoUtil.getDecodeData(realValue);
  93 + field.set(obj, value);
  94 + }
  95 + }
  96 + }
  97 + }
  98 + }
  99 + //long endTime=System.currentTimeMillis();
  100 + //log.info((isEncode ? "加密操作," : "解密操作,") + "当前程序耗时:" + (endTime - startTime) + "ms");
  101 + return obj;
  102 + }
  103 +
  104 + /**
  105 + * 处理 List<实体>
  106 + * @param obj
  107 + * @param entity
  108 + * @param isEncode(true: 加密操作 / false:解密操作)
  109 + */
  110 + public static void handleList(Object obj, Class entity, boolean isEncode){
  111 + List list = (List)obj;
  112 + if(list.size()>0){
  113 + Object first = list.get(0);
  114 + if(first.getClass().equals(entity)){
  115 + for(int i=0; i<list.size(); i++){
  116 + Object temp = list.get(i);
  117 + try {
  118 + handlerObject(temp, isEncode);
  119 + } catch (IllegalAccessException e) {
  120 + e.printStackTrace();
  121 + }
  122 + }
  123 + }
  124 + }
  125 + }
  126 +
  127 +
  128 + /**
  129 + * 处理数据 获取解密后的数据
  130 + * @param data
  131 + * @return
  132 + */
  133 + public static String getDecodeData(String data){
  134 + String result = null;
  135 + try {
  136 + result = AesEncryptUtil.desEncrypt(data);
  137 + } catch (Exception exception) {
  138 + log.warn("数据解密错误,原数据:"+data);
  139 + }
  140 + //解决debug模式下,加解密失效导致中文被解密变成空的问题
  141 + if(oConvertUtils.isEmpty(result) && oConvertUtils.isNotEmpty(data)){
  142 + result = data;
  143 + }
  144 + return result;
  145 + }
  146 +
  147 + /**
  148 + * 处理数据 获取加密后的数据 或是格式化后的数据
  149 + * @param data 字符串
  150 + * @param sensitiveEnum 类型
  151 + * @return 处理后的字符串
  152 + */
  153 + public static String getEncodeData(String data, SensitiveEnum sensitiveEnum){
  154 + String result;
  155 + switch (sensitiveEnum){
  156 + case ENCODE:
  157 + try {
  158 + result = AesEncryptUtil.encrypt(data);
  159 + } catch (Exception exception) {
  160 + log.error("数据加密错误", exception.getMessage());
  161 + result = data;
  162 + }
  163 + break;
  164 + case CHINESE_NAME:
  165 + result = chineseName(data);
  166 + break;
  167 + case ID_CARD:
  168 + result = idCardNum(data);
  169 + break;
  170 + case FIXED_PHONE:
  171 + result = fixedPhone(data);
  172 + break;
  173 + case MOBILE_PHONE:
  174 + result = mobilePhone(data);
  175 + break;
  176 + case ADDRESS:
  177 + result = address(data, 3);
  178 + break;
  179 + case EMAIL:
  180 + result = email(data);
  181 + break;
  182 + case BANK_CARD:
  183 + result = bankCard(data);
  184 + break;
  185 + case CNAPS_CODE:
  186 + result = cnapsCode(data);
  187 + break;
  188 + default:
  189 + result = data;
  190 + }
  191 + return result;
  192 + }
  193 +
  194 +
  195 + /**
  196 + * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
  197 + * @param fullName 全名
  198 + * @return <例子:李**>
  199 + */
  200 + private static String chineseName(String fullName) {
  201 + if (oConvertUtils.isEmpty(fullName)) {
  202 + return "";
  203 + }
  204 + return formatRight(fullName, 1);
  205 + }
  206 +
  207 + /**
  208 + * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号
  209 + * @param familyName 姓
  210 + * @param firstName 名
  211 + * @return <例子:李**>
  212 + */
  213 + private static String chineseName(String familyName, String firstName) {
  214 + if (oConvertUtils.isEmpty(familyName) || oConvertUtils.isEmpty(firstName)) {
  215 + return "";
  216 + }
  217 + return chineseName(familyName + firstName);
  218 + }
  219 +
  220 + /**
  221 + * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。
  222 + * @param id 身份证号
  223 + * @return <例子:*************5762>
  224 + */
  225 + private static String idCardNum(String id) {
  226 + if (oConvertUtils.isEmpty(id)) {
  227 + return "";
  228 + }
  229 + return formatLeft(id, 4);
  230 +
  231 + }
  232 +
  233 + /**
  234 + * [固定电话] 后四位,其他隐藏
  235 + * @param num 固定电话
  236 + * @return <例子:****1234>
  237 + */
  238 + private static String fixedPhone(String num) {
  239 + if (oConvertUtils.isEmpty(num)) {
  240 + return "";
  241 + }
  242 + return formatLeft(num, 4);
  243 + }
  244 +
  245 + /**
  246 + * [手机号码] 前三位,后四位,其他隐藏
  247 + * @param num 手机号码
  248 + * @return <例子:138******1234>
  249 + */
  250 + private static String mobilePhone(String num) {
  251 + if (oConvertUtils.isEmpty(num)) {
  252 + return "";
  253 + }
  254 + int len = num.length();
  255 + if(len<11){
  256 + return num;
  257 + }
  258 + return formatBetween(num, 3, 4);
  259 + }
  260 +
  261 + /**
  262 + * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护
  263 + * @param address 地址
  264 + * @param sensitiveSize 敏感信息长度
  265 + * @return <例子:北京市海淀区****>
  266 + */
  267 + private static String address(String address, int sensitiveSize) {
  268 + if (oConvertUtils.isEmpty(address)) {
  269 + return "";
  270 + }
  271 + int len = address.length();
  272 + if(len<sensitiveSize){
  273 + return address;
  274 + }
  275 + return formatRight(address, sensitiveSize);
  276 + }
  277 +
  278 + /**
  279 + * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示
  280 + * @param email 电子邮箱
  281 + * @return <例子:g**@163.com>
  282 + */
  283 + private static String email(String email) {
  284 + if (oConvertUtils.isEmpty(email)) {
  285 + return "";
  286 + }
  287 + int index = email.indexOf("@");
  288 + if (index <= 1){
  289 + return email;
  290 + }
  291 + String begin = email.substring(0, 1);
  292 + String end = email.substring(index);
  293 + String stars = "**";
  294 + return begin + stars + end;
  295 + }
  296 +
  297 + /**
  298 + * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号
  299 + * @param cardNum 银行卡号
  300 + * @return <例子:6222600**********1234>
  301 + */
  302 + private static String bankCard(String cardNum) {
  303 + if (oConvertUtils.isEmpty(cardNum)) {
  304 + return "";
  305 + }
  306 + return formatBetween(cardNum, 6, 4);
  307 + }
  308 +
  309 + /**
  310 + * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号
  311 + * @param code 公司开户银行联号
  312 + * @return <例子:12********>
  313 + */
  314 + private static String cnapsCode(String code) {
  315 + if (oConvertUtils.isEmpty(code)) {
  316 + return "";
  317 + }
  318 + return formatRight(code, 2);
  319 + }
  320 +
  321 +
  322 + /**
  323 + * 将右边的格式化成*
  324 + * @param str 字符串
  325 + * @param reservedLength 保留长度
  326 + * @return 格式化后的字符串
  327 + */
  328 + private static String formatRight(String str, int reservedLength){
  329 + String name = str.substring(0, reservedLength);
  330 + String stars = String.join("", Collections.nCopies(str.length()-reservedLength, "*"));
  331 + return name + stars;
  332 + }
  333 +
  334 + /**
  335 + * 将左边的格式化成*
  336 + * @param str 字符串
  337 + * @param reservedLength 保留长度
  338 + * @return 格式化后的字符串
  339 + */
  340 + private static String formatLeft(String str, int reservedLength){
  341 + int len = str.length();
  342 + String show = str.substring(len-reservedLength);
  343 + String stars = String.join("", Collections.nCopies(len-reservedLength, "*"));
  344 + return stars + show;
  345 + }
  346 +
  347 + /**
  348 + * 将中间的格式化成*
  349 + * @param str 字符串
  350 + * @param beginLen 开始保留长度
  351 + * @param endLen 结尾保留长度
  352 + * @return 格式化后的字符串
  353 + */
  354 + private static String formatBetween(String str, int beginLen, int endLen){
  355 + int len = str.length();
  356 + String begin = str.substring(0, beginLen);
  357 + String end = str.substring(len-endLen);
  358 + String stars = String.join("", Collections.nCopies(len-beginLen-endLen, "*"));
  359 + return begin + stars + end;
  360 + }
  361 +
  362 +}
... ...