1,注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TM {
/**
* 脱敏指定属性<br>
* 在TMStrategy.SELECT使用<br>
* 代表: @TMX中只需脱敏这些属性
*/
String[] sxs() default {};
/**
* 脱敏指定忽略属性<br>
* 在TMStrategy.SELECT使用<br>
* 代表: @TMX中不需脱敏这些属性
*/
String[] hlSxs() default {};
/**
* 脱敏深度<br>
* 在TMStrategy.DEFAULT使用<br>
* 代表: 默认深入@TMX成员类型一次 包括集合
*/
int depth() default 2;
/**
* 脱敏分组<br>
* 在TMStrategy.DEFAULT使用<br>
* 代表: 只对TMX中含此标识的进行脱敏
*/
String fenZu() default "";
/** 脱敏策略: 对不同字段类型【type】的统一脱敏方法 */
TMStrategy value() default TMStrategy.DEFAULT;
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TMX {
/** 脱敏字段类型 */
TMType value() default TMType.DEFAULT;
/** 脱敏替代字符 */
char replace() default '*';
/** 脱敏分组 以逗号分割 fenZus = {”getInfo“,"list"} */
String[] fenZus() default {};
}
2,环绕切面
@Aspect
@Component
public class TMAspect {
@Autowired
private TMAdapter handler;
//匹配所有OpenTuoMin,环绕通知可以修改返回值
@Around("@annotation(com.yaodong.common.security.annotation.TM)")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
TM openTuoMin = method.getDeclaredAnnotation(TM.class);
// ...执行前
// ...执行后
Object result = joinPoint.proceed();
if(result == null) return null;
if (result instanceof TableDataInfo) {
TableDataInfo info = (TableDataInfo) result;
handler.handle(info.getRows(),openTuoMin);
} else if(result instanceof AjaxResult){
AjaxResult info = (AjaxResult) result;
handler.handle(info.get("data"),openTuoMin);
} else if(result instanceof R){
R info = (R) result;
handler.handle(info.getData(),openTuoMin);
} else {
handler.handle(result, openTuoMin);
}
return result;
}
}
3,策略适配
/** 策略适配 */
@Component
public class TMAdapter {
@Autowired
@Qualifier("defaultStrategy")
TMStrategy defaultStrategy;
@Autowired
@Qualifier("selectStrategy")
TMStrategy selectStrategy;
/**
* @param org 脱敏(解密)实体
* @param openTuoMin 开启脱敏注解
*/
public <T> void handle(T org, TM openTuoMin) {
if(org == null) return;
com.yaodong.common.security.tuomin.constant.TMStrategy strategy = openTuoMin.value();
if(strategy.equals(com.yaodong.common.security.tuomin.constant.TMStrategy.DEFAULT)){
// 执行
defaultStrategy.executeAfter(org, openTuoMin);
}else if(strategy.equals(com.yaodong.common.security.tuomin.constant.TMStrategy.SELECT)){
// 执行
selectStrategy.executeAfter(org, openTuoMin);
}
}
}
4,策略接口
public interface TMStrategy {
// 无效的字符串 字段和脱敏类型格式冲突 TODO 可在【对应类型】处理办法补充
String valid = "";
/**
* 不同策略的遍历行为不同 默认深度优先遍历
* @param target 需脱敏对象
* @param openTuoMin 方法上的脱敏注解
*/
<T> void executeAfter(T target, TM openTuoMin);
/**
* @param target 需解密对象
* @param openTuoMin 方法上的脱敏注解
*/
<T> void executeBefore(T target, TM openTuoMin);
/**=============================== 脱敏方法 ============================*/
static String desensitizeName(String name, char replace) {
int length = name.length();
if (length == 2) {
return name.substring(0, 1) + replace;
} else if (length == 3) {
return name.substring(0, 1) + replace + name.substring(2);
}
return valid; // 其他长度返回无效结果
}
static String desensitizeEmail(String email, char replace) {
int index = email.indexOf('@');
if (index == -1 || index < 8) {
return email; // 如果@符号不存在或位置不对,返回原字符串
}
// 例如 155****922@qq.com
return email.substring(0, 3) + new String(new char[index - 7]).replace('\0', replace) + email.substring(index - 4);
}
static String desensitizePhoneNumber(String phoneNumber, char replace) {
//电话位数
if (phoneNumber.length() == 11) {
// 中国大陆
return phoneNumber.substring(0,3) +new String(new char[4]).replace('\0', replace) + phoneNumber.substring(7,11);
}else if(phoneNumber.length() == 7){
// 座机
return phoneNumber.substring(0,3) +new String(new char[2]).replace('\0', replace) + phoneNumber.substring(5,7);
}else if(phoneNumber.length() == 10){
// 台湾
return phoneNumber.substring(0,3) +new String(new char[3]).replace('\0', replace) + phoneNumber.substring(6,10);
}
return valid; // 长度不对返回无效结果
}
static String desensitizeCreditCode(String of, char replace) {
// 营业执照 18位 后六位
if(of.length() != 16)
{
return valid;
}
return of.replace(of.substring(12,18),new String(new char[6]).replace('\0', replace));
}
static String desensitizeAddress(String address, char replace) {
// 汉字数字的正则表达式(这里仅列举了部分,可以根据需要添加更多)
String chineseNumbers = "一|二|三|四|五|六|七|八|九|十";
Pattern pattern = Pattern.compile(chineseNumbers);
Matcher matcher = pattern.matcher(address);
address = matcher.replaceAll("*");
return address.replaceAll("\\d", String.valueOf(replace)); //只做数字替换
}
// 例如,IDENTITY_CARD可能如下:
static String desensitizeIdentityCard(String idCard, char replace) {
if (idCard.length() != 18) {
return valid; // 长度不对返回无效结果
}
return idCard.substring(0, 6) + new String(new char[8]).replace('\0', replace) +idCard.substring(0, 4); // 显示前14位,后四位用*替换
}
// 检查对象类型
static TMFieldType checkClassType(Object instance){
if(instance.getClass().isPrimitive()){
return TMFieldType.DEFAULT;
}else if(instance instanceof Integer || instance instanceof Long || instance instanceof Float || instance instanceof Double ||
instance instanceof Character || instance instanceof Byte || instance instanceof Short || instance instanceof String){
return TMFieldType.DEFAULT;
}else if(instance instanceof Date || instance instanceof BigDecimal || instance instanceof Calendar){
return TMFieldType.DEFAULT;
}
else if(instance instanceof Collection){
return TMFieldType.COLLECTION;
}else {
return TMFieldType.ENTITY;
}
}
/** 获取类及其所有父类声明的字段 */
static Field[] getAllFields(Class<?> clazz) {
List<Field> fieldsList = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null) {
// 获取当前类声明的所有字段
Field[] declaredFields = currentClass.getDeclaredFields();
// 将这些字段添加到列表中
for (Field field : declaredFields) {
fieldsList.add(field);
}
// 移至父类
currentClass = currentClass.getSuperclass();
}
// 将列表转换为数组
return fieldsList.toArray(new Field[0]);
}
/**
* 处理方法 <p>可以扩充类型,加上处理办法<p/>
* @param originalValue 需要脱敏属性
* @param type 类型
* @param replace 替代字符
* @return 脱敏后属性
*/
static String start(Object originalValue, TMType type, char replace) {
if (originalValue == null || "null".equals(String.valueOf(originalValue))) {
return valid; // 无效结果
}
String of = String.valueOf(originalValue);
switch (type) {
case NAME:
return TMStrategy.desensitizeName(of, replace);
case EMAIL:
return TMStrategy.desensitizeEmail(of, replace);
case ADDRESS:
return TMStrategy.desensitizeAddress(of, replace);
case CREDIT_CODE:
return TMStrategy.desensitizeCreditCode(of,replace);
case PHONE_NUMBER:
return TMStrategy.desensitizePhoneNumber(of, replace);
case IDENTITY_CARD:
return TMStrategy.desensitizeIdentityCard(of,replace);
default:
return valid; // 无效结果或未知类型
}
}
/**
* 处理方法 <p>可以扩充类型Constant.Type,加上处理办法<p/>
* @param originalValue 需要脱敏属性
* @param type 类型
* @return 脱敏后属性
*/
static String start(Object originalValue, TMType type) {
return start(originalValue, type,'*');
}
}
5,两个策略
@Component("selectStrategy")
public class TMSelectStrategy implements TMStrategy {
private static final Logger logger = LoggerFactory.getLogger(TMDefaultStrategy.class);
@Override
public <T> void executeAfter(T target, TM openTuoMin) {
traverse(target, null, null, openTuoMin);
}
@Override
public <T> void executeBefore(T target, TM openTuoMin) { }
/**
* @param target 当前检查对象 为null时,代表本身在下一次循环中不会被修改
* @param use 当前检查对象属性 为null时,代表target在下一次循环中不会被修改
* @param source 来源对象 当检查对象时脱敏属性用于修改
* @param openTuoMin 方法注解 存储了过滤信息
*/
public static void traverse(Object target, Object source, Field use, TM openTuoMin) {
if (target == null) {
return;
}
TMFieldType fieldType = TMStrategy.checkClassType(target);
switch (fieldType){
case COLLECTION:{// 检查是否为集合类型
for (Object element : (Collection<?>) target) {
traverse(element, null, null, openTuoMin);
}
break;
}
case ENTITY:{// 检查是否为其他类的实例,并递归遍历
// 获取所有属性
Field[] fields = TMStrategy.getAllFields(target.getClass());
for (Field field : fields) {
try {
field.setAccessible(true);
Object value = field.get(target);
traverse(value, target, field, openTuoMin);
} catch (IllegalAccessException e) {
logger.error("【脱敏】属性遍历异常...",e);
}finally {
field.setAccessible(false);
}
}
break;
}
case DEFAULT:// 真实属性值
default:
// 对象 属性 属性对象 方法上注解
if(use == null)return;
handle(source, use, target, openTuoMin.sxs(), openTuoMin.hlSxs());
}
}
/**
* 原始基础类递归,扫描内部属性,进行数据托敏
* @param target 上层脱敏对象
* @param sxs 指定脱敏的属性名
* @param field 内部需要脱敏的字段
*/
protected static void handle(Object target, Field field, Object value, String[] sxs, String[] hlSxs) {
try {
if(target == null) return;
if(field.getType() != String.class) return;
// 检查字段上是否有TM注解
TMX tmAnnotation = field.getDeclaredAnnotation(TMX.class);
if (tmAnnotation == null) return;
// 是否在TM的指定的属性中
if(!(sxs.length != 0 && Arrays.asList(sxs).stream().anyMatch(sx -> sx.equals(field.getName())))) return;
// 是否不在TM的指定的忽略属性中
if(hlSxs.length != 0 && Arrays.asList(hlSxs).stream().anyMatch(hlsx -> hlsx.equals(field.getName()))) return;
TMType type = tmAnnotation.value();
char replace = tmAnnotation.replace();
// 脱敏
Object res = TMStrategy.start(value, type, replace);
field.set(target, res);
} catch (Exception e) {
logger.error("【脱敏】属性遍历异常...",e);
} finally {
field.setAccessible(false);
}
}
}
@Component("defaultStrategy")
public class TMDefaultStrategy implements TMStrategy {
private static final Logger logger = LoggerFactory.getLogger(TMDefaultStrategy.class);
@Override
public <T> void executeAfter(T target, TM openTm) {
// 校验传参
if(openTm.depth() < 0){
throw new RuntimeException("开启脱敏 参数非法!");
}
if(target == null) return;
try {
// 初始只为 实体类/集合类
selector(null,null, target, openTm.fenZu(), openTm.depth());
} catch (InterruptedException e) {
logger.info("【脱敏】大数据量处理异常...");
}
}
@Override
public <T> void executeBefore(T target, TM openTm){}
/**
* 判断inner类型,选择不同的递归方式
* @param entity 上层脱敏对象
* @param field entity内部需要脱敏的字段
* @param inner entity内部需要脱敏的对象
* @param groupId 开启脱敏的分组id
* @param level 脱敏的递归层级
* @param <T> 保存数据的原始类型
* @throws InterruptedException 由大数据量list处理式的线程池抛出
*/
private void selector(Object entity, Field field, Object inner, String groupId, int level) throws InterruptedException {
TMFieldType fieldType = TMStrategy.checkClassType(inner);// 处理集合字段
if (fieldType == TMFieldType.COLLECTION) {
// 处理集合
handleCollectionClass((Collection<?>) inner, groupId, level);
} else if (fieldType == TMFieldType.ENTITY) {
// 处理实体字段
handlerSelfClass(inner, groupId, level);
} else {
// 处理包装类和基本类
handlerBaseClass(entity, field, groupId);
}
}
/**
* 集合递归方式,获取内部实体,交替回handleField
* @param collection 经instanceof判断集合
* @param groupId 开启脱敏的分组id
* @param level 脱敏的递归剩余层级
* @throws InterruptedException 处理式的线程池抛出
*/
protected void handleCollectionClass(Collection<?> collection, String groupId, int level) throws InterruptedException {
ExecutorService service = null;
try {
// 数据比较多时,使用线程池跑
if (collection.size() > 1000) {
service = Executors.newFixedThreadPool(5);
ExecutorService finalService = service;
collection.parallelStream().forEach(entity -> {
try {
finalService.submit(() -> handlerSelfClass(entity, groupId, level));
} catch (Exception e) {
logger.info("【脱敏】大数据量处理失败...");
}
});
service.shutdown();
if (!service.awaitTermination(1, TimeUnit.MINUTES)) {
service.shutdownNow();
}
} else {
collection.forEach(entity -> handlerSelfClass(entity, groupId, level));
}
} finally {
if (service != null && !service.isTerminated()) {
service.shutdownNow();
}
}
}
/**
* 实体类递归,扫描内部属性,交回交替回handleField
* @param entity 上层脱敏对象
* @param groupId 开启脱敏的分组id
* @param level 脱敏的递归剩余层级
*/
protected void handlerSelfClass(Object entity, String groupId, int level){
if(level < 1) return;
// 获取当前对象的所有字段,包括继承
Field[] fields = TMStrategy.getAllFields(entity.getClass());
for (Field field : fields) {
try {
field.setAccessible(true);
// 此时inner已经失去原本类型
Object inner = field.get(entity);
if (inner != null) {
selector(entity, field, inner, groupId, level - 1);
}
} catch (IllegalAccessException | InterruptedException e) {
logger.info("【脱敏】属性遍历异常...");
} finally {
field.setAccessible(false);
}
}
}
/**
* 原始基础类递归,扫描内部属性,进行数据托敏
* @param target 上层脱敏对象
* @param groupId 开启脱敏的分组id
* @param field 内部需要脱敏的字段
*/
protected void handlerBaseClass(Object target, Field field, String groupId) {
try {
// 若需处理非字节类型,需要对返回值类型生成代理对象
if(target == null) return;
if(field.getType() != String.class) return;
// 检查字段上是否有TM注解
TMX tmAnnotation = field.getDeclaredAnnotation(TMX.class);
if (tmAnnotation == null) return;
// 脱敏条件 不配置放行/包含放行
String[] groupIds = tmAnnotation.fenZus();
boolean shouldDesensitize =
groupIds.length == 0 ||
groupId.isEmpty() ||
Arrays.stream(groupIds).anyMatch(id -> id.equals(groupId));
if (!shouldDesensitize) return;
TMType type = tmAnnotation.value();
char replace = tmAnnotation.replace();
Object o = field.get(target);
//脱敏
Object res = TMStrategy.start(o, type, replace);
field.set(target, res);
} catch (Exception e) {
logger.info("【脱敏】属性遍历异常...");
} finally {
field.setAccessible(false);
}
}
}