Commit 2be6e8845dbdd1a18e20c264b19ebe894b929d39
1 parent
c057fbcd
数据脱敏注解
Showing
6 changed files
with
559 additions
and
0 deletions
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 | +} | |
... | ... |