6 changed files with 500 additions and 2 deletions
@ -0,0 +1,35 @@ |
|||||
|
package com.elink.esua.epdc.commons.tools.annotation; |
||||
|
|
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
/** |
||||
|
* 标记一个接口,它的返回值中的某些字段需要打掩码 |
||||
|
* |
||||
|
* @author zqf |
||||
|
*/ |
||||
|
@Target(ElementType.METHOD) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Documented |
||||
|
public @interface MaskResponse { |
||||
|
|
||||
|
/** |
||||
|
* 掩码类型 |
||||
|
*/ |
||||
|
String MASK_TYPE_ID_CARD = "ID_CARD"; |
||||
|
String MASK_TYPE_MOBILE = "MOBILE"; |
||||
|
String MASK_TYPE_CHINESE_NAME = "CHINESE_NAME"; |
||||
|
|
||||
|
/** |
||||
|
* 要打码的字段列表。会递归的着这些字段 |
||||
|
* |
||||
|
* @return |
||||
|
*/ |
||||
|
String[] fieldNames() default {"idCard", "mobile", "phone"}; |
||||
|
|
||||
|
/** |
||||
|
* 要打码的类型 |
||||
|
* |
||||
|
* @return |
||||
|
*/ |
||||
|
String[] fieldsMaskType() default {MASK_TYPE_ID_CARD, MASK_TYPE_MOBILE, MASK_TYPE_MOBILE}; |
||||
|
} |
@ -0,0 +1,38 @@ |
|||||
|
package com.elink.esua.epdc.commons.tools.aspect; |
||||
|
|
||||
|
import com.elink.esua.epdc.commons.tools.annotation.MaskResponse; |
||||
|
import com.elink.esua.epdc.commons.tools.exception.RenException; |
||||
|
import com.elink.esua.epdc.commons.tools.processor.MaskProcessor; |
||||
|
import com.elink.esua.epdc.commons.tools.utils.Result; |
||||
|
import org.aspectj.lang.JoinPoint; |
||||
|
import org.aspectj.lang.annotation.AfterReturning; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
import org.aspectj.lang.reflect.MethodSignature; |
||||
|
import org.springframework.core.annotation.Order; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
/** |
||||
|
* @author Administrator |
||||
|
*/ |
||||
|
@Aspect |
||||
|
@Component |
||||
|
@Order(0) |
||||
|
public class MaskResponseAspect { |
||||
|
|
||||
|
@AfterReturning(pointcut = "@annotation(com.elink.esua.epdc.commons.tools.annotation.MaskResponse)", returning = "result") |
||||
|
public Object proceed(JoinPoint point, Result result) throws Throwable { |
||||
|
MethodSignature signature = (MethodSignature) point.getSignature(); |
||||
|
MaskResponse maskResponseAnno = signature.getMethod().getAnnotation(MaskResponse.class); |
||||
|
|
||||
|
String[] fieldNames = maskResponseAnno.fieldNames(); |
||||
|
String[] fieldsMaskType = maskResponseAnno.fieldsMaskType(); |
||||
|
|
||||
|
if (fieldNames.length != fieldsMaskType.length) { |
||||
|
String msg = "掩码配置错误"; |
||||
|
throw new RenException(msg); |
||||
|
} |
||||
|
|
||||
|
new MaskProcessor(fieldNames, fieldsMaskType).mask(result); |
||||
|
return null; |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
package com.elink.esua.epdc.commons.tools.enums; |
||||
|
|
||||
|
/** |
||||
|
* 唯一整件类型 |
||||
|
*/ |
||||
|
public enum IdCardTypeEnum { |
||||
|
|
||||
|
OTHERS("0", "其他"), |
||||
|
SFZH("1", "身份证号"), |
||||
|
PASSPORT("2", "护照"); |
||||
|
|
||||
|
private String type; |
||||
|
private String name; |
||||
|
|
||||
|
IdCardTypeEnum(String type, String name) { |
||||
|
this.type = type; |
||||
|
this.name = name; |
||||
|
} |
||||
|
|
||||
|
public String getType() { |
||||
|
return type; |
||||
|
} |
||||
|
|
||||
|
public String getName() { |
||||
|
return name; |
||||
|
} |
||||
|
} |
@ -0,0 +1,239 @@ |
|||||
|
package com.elink.esua.epdc.commons.tools.processor; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.elink.esua.epdc.commons.tools.annotation.MaskResponse; |
||||
|
import com.elink.esua.epdc.commons.tools.enums.IdCardTypeEnum; |
||||
|
import com.elink.esua.epdc.commons.tools.exception.ExceptionUtils; |
||||
|
import com.elink.esua.epdc.commons.tools.page.PageData; |
||||
|
import com.elink.esua.epdc.commons.tools.utils.IdCardRegexUtils; |
||||
|
import com.elink.esua.epdc.commons.tools.utils.Result; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.springframework.util.CollectionUtils; |
||||
|
|
||||
|
import java.lang.reflect.Field; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* desc:脱敏处理器 |
||||
|
* |
||||
|
* @author Administrator |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class MaskProcessor { |
||||
|
|
||||
|
|
||||
|
public static final String EPMET_PACKAGE_PREFIX = "com.elink.esua.epdc"; |
||||
|
|
||||
|
private List<String> fieldNames; |
||||
|
private List<String> fieldsMaskType; |
||||
|
|
||||
|
public MaskProcessor(String[] fields, String[] fieldsMaskType) { |
||||
|
if (fields != null && fields.length > 0) { |
||||
|
this.fieldNames = Arrays.asList(fields); |
||||
|
this.fieldsMaskType = Arrays.asList(fieldsMaskType); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 唯一整件号打码,可能是身份证号或者是护照号 |
||||
|
* 将明文字符串打码变为掩码。保留前6,后面打码 |
||||
|
* |
||||
|
* @param originString |
||||
|
* @return |
||||
|
*/ |
||||
|
public static String maskIdCard(String originString) { |
||||
|
|
||||
|
IdCardRegexUtils regexUtil = IdCardRegexUtils.parse(originString); |
||||
|
if (regexUtil == null) { |
||||
|
// 不匹配任何类型,不码
|
||||
|
return originString; |
||||
|
} |
||||
|
|
||||
|
if (regexUtil.getTypeEnum() == IdCardTypeEnum.SFZH) { |
||||
|
// 身份证号
|
||||
|
// 仅将6位之后的全都打码
|
||||
|
int maskedTextLength = 12; |
||||
|
int length = originString.length(); |
||||
|
String maskStr = StrUtil.repeatByLength("*", length - maskedTextLength); |
||||
|
return originString.replaceAll("^(\\d{10})\\d+([a-zA-Z0-9]{2})$", new StringBuilder("$1").append(maskStr).append("$2").toString()); |
||||
|
} else if (regexUtil.getTypeEnum() == IdCardTypeEnum.PASSPORT) { |
||||
|
// 护照,前两位,后两位为明文,其他*
|
||||
|
int clearLength = 4; |
||||
|
int maskedLength = 0; |
||||
|
if ((maskedLength = originString.length() - clearLength) > 0) { |
||||
|
String maskStr = StrUtil.repeatByLength("*", maskedLength); |
||||
|
return originString.replaceAll("^([a-zA-Z0-9]{2})[a-zA-Z0-9]+([a-zA-Z0-9]{2})$", new StringBuilder("$1").append(maskStr).append("$2").toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return originString; |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
String[] idc = {"idCard"}; |
||||
|
String[] idct = {MaskResponse.MASK_TYPE_ID_CARD}; |
||||
|
String r = new MaskProcessor(idc, idct).maskString("王五(372284152412022222)", MaskResponse.MASK_TYPE_ID_CARD); |
||||
|
System.out.println(r); |
||||
|
String s = MaskProcessor.maskIdCard("372284152412022222"); |
||||
|
System.out.println(s); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为dto中的属性打掩码 |
||||
|
* |
||||
|
* @param object |
||||
|
*/ |
||||
|
public void mask(Object object) { |
||||
|
if (object == null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (object instanceof Result) { |
||||
|
mask(((Result<?>) object).getData()); |
||||
|
} else if (object instanceof PageData) { |
||||
|
mask(((PageData<?>) object).getList()); |
||||
|
} else if (object instanceof List) { |
||||
|
((List) object).forEach(e -> mask(e)); |
||||
|
} else if (object instanceof Map) { |
||||
|
maskMap((Map) object); |
||||
|
} else if (object.getClass().getName().startsWith(EPMET_PACKAGE_PREFIX)) { |
||||
|
// 自定义bean,走反射
|
||||
|
maskEpmetBean(object); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 为map打码,只打value中的码 |
||||
|
* - 如果value是epmet的dto,那么去反射它 |
||||
|
* - 如果value是字符串,那么直接给他打码 |
||||
|
* - 如果value是其他类型,跳过 |
||||
|
* |
||||
|
* @param map |
||||
|
*/ |
||||
|
private void maskMap(Map<Object, Object> map) { |
||||
|
if (CollectionUtils.isEmpty(map)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
for (Map.Entry<Object, Object> entry : map.entrySet()) { |
||||
|
Object value = entry.getValue(); |
||||
|
Object key = entry.getKey(); |
||||
|
if (value != null && value.getClass().getName().startsWith(EPMET_PACKAGE_PREFIX)) { |
||||
|
// 是epmet的对象
|
||||
|
maskEpmetBean(value); |
||||
|
} else if (value instanceof String) { |
||||
|
int index = fieldNames.indexOf(key); |
||||
|
if (index != -1) { |
||||
|
String maskResult = maskString((String) value, fieldsMaskType.get(index)); |
||||
|
entry.setValue(maskResult); |
||||
|
} |
||||
|
} else if (value instanceof List) { |
||||
|
// 列表
|
||||
|
((List) value).forEach(e -> mask(e)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 反射 |
||||
|
* |
||||
|
* @param object |
||||
|
*/ |
||||
|
private void maskEpmetBean(Object object) { |
||||
|
Field[] declaredFields = object.getClass().getDeclaredFields(); |
||||
|
for (Field currentField : declaredFields) { |
||||
|
currentField.setAccessible(true); |
||||
|
try { |
||||
|
String fieldName = currentField.getName(); |
||||
|
Object value = currentField.get(object); |
||||
|
// 是epmet的类,继续下钻
|
||||
|
if (currentField.getClass().getName().startsWith(EPMET_PACKAGE_PREFIX)) { |
||||
|
maskEpmetBean(value); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 是字符串
|
||||
|
String fieldValue; |
||||
|
if (value instanceof String && StringUtils.isNotBlank(fieldValue = (String) value)) { |
||||
|
int fieldIndexInAnnoAttrs = fieldNames.indexOf(fieldName); |
||||
|
if (fieldIndexInAnnoAttrs != -1) { |
||||
|
String product = maskString(fieldValue, fieldsMaskType.get(fieldIndexInAnnoAttrs)); |
||||
|
currentField.set(object, product); |
||||
|
} |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 非字符串,非epmet类的其他类型
|
||||
|
mask(value); |
||||
|
} catch (IllegalAccessException e) { |
||||
|
log.error("【mask一些字段报错】{}", ExceptionUtils.getErrorStackTrace(e)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 把字符串变更为掩码 |
||||
|
* |
||||
|
* @param originString |
||||
|
* @return |
||||
|
*/ |
||||
|
public String maskString(String originString, String maskType) { |
||||
|
if (MaskResponse.MASK_TYPE_ID_CARD.equals(maskType)) { |
||||
|
return maskIdCard(originString); |
||||
|
} else if (MaskResponse.MASK_TYPE_MOBILE.equals(maskType)) { |
||||
|
return maskMobile(originString); |
||||
|
} else if (MaskResponse.MASK_TYPE_CHINESE_NAME.equals(maskType)) { |
||||
|
return maskChineseName(originString); |
||||
|
} else { |
||||
|
return originString; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 对中文人名进行打码 |
||||
|
* |
||||
|
* @param originString |
||||
|
* @return |
||||
|
*/ |
||||
|
private String maskChineseName(String originString) { |
||||
|
if (StringUtils.isBlank(originString)) { |
||||
|
// 空串,或者只有一个字的,不打码,直接返回
|
||||
|
return originString; |
||||
|
} |
||||
|
|
||||
|
int length = originString.length(); |
||||
|
// 2个字以上的,首位字母明文,中间*
|
||||
|
// 中文不能用\\w,要用[\u4e00-\u9fa5]
|
||||
|
if (length == 2) { |
||||
|
// return originString.replaceAll("^([\\u4e00-\\u9fa5]).*$", "$1*");
|
||||
|
return originString.substring(0).concat("*"); |
||||
|
} else { |
||||
|
String maskStr = StrUtil.repeat("*", length - 2); |
||||
|
// return originString.replaceAll("^([\\u4e00-\\u9fa5]).*([\\u4e00-\\u9fa5])$", "$1" + maskStr + "$2");
|
||||
|
return originString.charAt(0) + maskStr + originString.charAt(originString.length() - 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将明文字符串打码变为掩码。保留前3后4,中间打码 |
||||
|
* 187****3461 |
||||
|
* |
||||
|
* @param originString |
||||
|
* @return |
||||
|
*/ |
||||
|
private String maskMobile(String originString) { |
||||
|
int length = originString.length(); |
||||
|
if (length <= 7) { |
||||
|
return originString; |
||||
|
} |
||||
|
|
||||
|
String maskStr = StrUtil.repeatByLength("*", length - 7); |
||||
|
if (length != 11) { |
||||
|
return StringUtils.leftPad(StringUtils.right(originString, 4), length, "*"); |
||||
|
} |
||||
|
return originString.replaceAll("^(1\\d{2})\\d*(\\d{4})$", new StringBuilder("$1").append(maskStr).append("$2").toString()); |
||||
|
} |
||||
|
} |
@ -0,0 +1,159 @@ |
|||||
|
package com.elink.esua.epdc.commons.tools.utils; |
||||
|
|
||||
|
import com.elink.esua.epdc.commons.tools.enums.IdCardTypeEnum; |
||||
|
import com.elink.esua.epdc.commons.tools.exception.ExceptionUtils; |
||||
|
import com.elink.esua.epdc.commons.tools.exception.RenException; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.time.DateTimeException; |
||||
|
import java.time.LocalDate; |
||||
|
import java.time.Period; |
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
/** |
||||
|
* 唯一整件正则工具 |
||||
|
*/ |
||||
|
public class IdCardRegexUtils { |
||||
|
|
||||
|
/** |
||||
|
* 15位身份证号的正则表达式 |
||||
|
*/ |
||||
|
private static final Pattern PATTERN_15_ID = Pattern.compile("^\\d{6}(?<year>\\d{2})(?<month>0[1-9]|1[0-2])(?<day>[0-2][0-9]|3[0-1])\\d{2}(?<sex>\\d)$"); |
||||
|
/** |
||||
|
* 18位身份证号的正则表达式 |
||||
|
*/ |
||||
|
private static final Pattern PATTERN_18_ID = Pattern.compile("^\\d{6}(?<year>\\d{4})(?<month>0[1-9]|1[0-2])(?<day>[0-2][0-9]|3[0-1])\\d{2}(?<sex>\\d)[0-9a-xA-X]$"); |
||||
|
|
||||
|
/** |
||||
|
* 9位护照 |
||||
|
*/ |
||||
|
private static final Pattern PATTERN_9_PASSPORT = Pattern.compile("^[a-zA-Z0-9]{8,9}$"); |
||||
|
|
||||
|
private String inputText; |
||||
|
|
||||
|
private Matcher matcher; |
||||
|
|
||||
|
private IdCardTypeEnum idCardType; |
||||
|
|
||||
|
private IdCardRegexUtils(IdCardTypeEnum idCardType, Matcher matcher, String inputText) { |
||||
|
this.idCardType = idCardType; |
||||
|
this.matcher = matcher; |
||||
|
this.inputText = inputText; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* desc:校验输入的证件号是否合法 |
||||
|
* |
||||
|
* @param input |
||||
|
* @return |
||||
|
*/ |
||||
|
public static boolean validateIdCard(String input) { |
||||
|
IdCardRegexUtils parse = IdCardRegexUtils.parse(input); |
||||
|
return parse != null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析正则 |
||||
|
* |
||||
|
* @param input |
||||
|
* @return |
||||
|
*/ |
||||
|
public static IdCardRegexUtils parse(String input) { |
||||
|
if (input == null || input.trim().length() == 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (input.length() == 15) { |
||||
|
Matcher matcher = PATTERN_15_ID.matcher(input); |
||||
|
if (matcher.matches()) { |
||||
|
return new IdCardRegexUtils(IdCardTypeEnum.SFZH, matcher, input); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (input.length() == 18) { |
||||
|
Matcher matcher = PATTERN_18_ID.matcher(input); |
||||
|
if (matcher.matches()) { |
||||
|
return new IdCardRegexUtils(IdCardTypeEnum.SFZH, matcher, input); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (input.length() == 9 || input.length() == 8) { |
||||
|
Matcher matcher = PATTERN_9_PASSPORT.matcher(input); |
||||
|
if (matcher.matches()) { |
||||
|
return new IdCardRegexUtils(IdCardTypeEnum.PASSPORT, matcher, input); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
IdCardRegexUtils parse = IdCardRegexUtils.parse("370282198801303017"); |
||||
|
ParsedContent parsedResult = parse.getParsedResult(); |
||||
|
System.out.println(parsedResult); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取解析结果 |
||||
|
* |
||||
|
* @return |
||||
|
*/ |
||||
|
public ParsedContent getParsedResult() { |
||||
|
if (matcher == null || idCardType == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (IdCardTypeEnum.SFZH == idCardType) { |
||||
|
//是身份证号,可以解析
|
||||
|
String year; |
||||
|
if (inputText.length() == 15) { |
||||
|
// 15位身份证号,years前需要拼上19
|
||||
|
year = "19".concat(matcher.group("year")); |
||||
|
} else { |
||||
|
year = matcher.group("year"); |
||||
|
} |
||||
|
String month = matcher.group("month"); |
||||
|
String day = matcher.group("day"); |
||||
|
String sex = matcher.group("sex"); |
||||
|
|
||||
|
// ------- 年龄Start----------
|
||||
|
Integer age; |
||||
|
try { |
||||
|
LocalDate birthday = LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)); |
||||
|
age = Period.between(birthday, LocalDate.now()).getYears(); |
||||
|
} catch (DateTimeException e) { |
||||
|
throw new RenException("身份证号解析年龄失败:" + ExceptionUtils.getErrorStackTrace(e)); |
||||
|
} |
||||
|
// ------- 年龄End----------
|
||||
|
return new ParsedContent(year, month, day, sex, age); |
||||
|
} |
||||
|
|
||||
|
// 其他类型暂时不可解析
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取类型枚举 |
||||
|
* |
||||
|
* @return |
||||
|
*/ |
||||
|
public IdCardTypeEnum getTypeEnum() { |
||||
|
return idCardType; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 正则解析结果 |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public static class ParsedContent { |
||||
|
private String birthdayYear; |
||||
|
private String birthdayMonth; |
||||
|
private String birthdayDay; |
||||
|
private String sex; |
||||
|
private Integer age; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue