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 | +} |