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