diff --git a/epdc-commons-tools/pom.xml b/epdc-commons-tools/pom.xml index 969db43..c42f989 100644 --- a/epdc-commons-tools/pom.xml +++ b/epdc-commons-tools/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -16,7 +16,7 @@ 6.0.12.Final 3.7 1.3.3 - 4.1.8 + 4.6.1 4.1.0 2.9.9 1.2.59 diff --git a/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/annotation/MaskResponse.java b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/annotation/MaskResponse.java new file mode 100644 index 0000000..e3199ac --- /dev/null +++ b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/annotation/MaskResponse.java @@ -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}; +} diff --git a/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/aspect/MaskResponseAspect.java b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/aspect/MaskResponseAspect.java new file mode 100644 index 0000000..0a0acbc --- /dev/null +++ b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/aspect/MaskResponseAspect.java @@ -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; + } +} diff --git a/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/enums/IdCardTypeEnum.java b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/enums/IdCardTypeEnum.java new file mode 100644 index 0000000..6e5c0b7 --- /dev/null +++ b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/enums/IdCardTypeEnum.java @@ -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; + } +} diff --git a/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/processor/MaskProcessor.java b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/processor/MaskProcessor.java new file mode 100644 index 0000000..eca25a2 --- /dev/null +++ b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/processor/MaskProcessor.java @@ -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 fieldNames; + private List 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 map) { + if (CollectionUtils.isEmpty(map)) { + return; + } + + for (Map.Entry 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()); + } +} diff --git a/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/utils/IdCardRegexUtils.java b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/utils/IdCardRegexUtils.java new file mode 100644 index 0000000..8d9ffd1 --- /dev/null +++ b/epdc-commons-tools/src/main/java/com/elink/esua/epdc/commons/tools/utils/IdCardRegexUtils.java @@ -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}(?\\d{2})(?0[1-9]|1[0-2])(?[0-2][0-9]|3[0-1])\\d{2}(?\\d)$"); + /** + * 18位身份证号的正则表达式 + */ + private static final Pattern PATTERN_18_ID = Pattern.compile("^\\d{6}(?\\d{4})(?0[1-9]|1[0-2])(?[0-2][0-9]|3[0-1])\\d{2}(?\\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; + } +}