Spring Boot 可以结合 注解 + 反射 + AOP(或 Jackson 序列化) 来实现数据脱敏。你可以使用 自定义注解 标记需要脱敏的字段,并在数据返回时通过 AOPJackson 序列化 进行处理

 注意事项:项目中使用jackson,是无法细粒度控制在列表接口脱敏,详情接口不脱敏的。如果想要这样使用,使用多个vo区分列表和详情。

如使用aop脱敏,只在特定类中对实体类字段进行脱敏,使用aop,将脱敏的包隔开。

但这2种方式都无法解决导出时数据脱敏,导出excel只能手动调用。例如:

1.aop

启动类加@EnableAspectJAutoProxy

自定义注解,在实体类中使用表示被脱敏字段

建立aop切面类

//这里是上面的maskdata方法的复杂版,因为项目中的对象都是嵌套包裹的 
private void desensitizeFields( Object obj) throws IllegalAccessException {
        if (obj == null) return;

        // 如果对象是一个Map类型
        if (obj instanceof Map<?, ?>) {
            Map<?, ?> map = (Map<?, ?>) obj;
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                Object value = entry.getValue();
                if (value != null) {
                    // 递归处理Map中的对象
                    desensitizeFields(value);
                }
            }
            return;
        }

        // 如果对象是一个List类型
        if (obj instanceof List<?>) {
            List<?> list = (List<?>) obj;
            for (Object item : list) {
                if (item != null) {
                    // 递归处理List中的每个对象
                    desensitizeFields(item);
                }
            }
            return;
        }

        // 处理对象的字段
        Class<?> clazz = obj.getClass();
        List<Field> fields = new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()));
        clazz = clazz.getSuperclass();
        if (clazz!=null){
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
        }
        for (Field field : fields) {
            if (Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true);
            Object valueOrigin =  field.get(obj);
            if (valueOrigin!=null&&field.isAnnotationPresent(SensitiveData.class)) {
                SensitiveData annotation = field.getAnnotation(SensitiveData.class);
                // 注意:这里需要设置field为可访问
                // 根据字段类型执行不同的脱敏逻辑
                SensitiveTypeEnum sensitiveTypeEnum = annotation.value();
                String value = valueOrigin.toString();
                switch (sensitiveTypeEnum) {
                    case COMMON:
                        value = MsgDesensitizedUtil.commonStr(value);
                        break;
                    case ID_CARD:
                        value = MsgDesensitizedUtil.idCardNum(value);
                        break;
                    case PHONE_NUMBER:
                        value = MsgDesensitizedUtil.mobilePhone(value);
                        break;
                    case EMAIL:
                        value = MsgDesensitizedUtil.email(value);
                        break;
                    default:
                        throw new RuntimeException("未知脱敏类型");
                }
                field.set(obj, value);
            } else if (valueOrigin != null && !isPrimitiveOrWrapper(field.getType())) {
                // 如果字段是一个对象(非基本类型),递归处理
                desensitizeFields(valueOrigin);
            }
        }
    }

    private boolean isPrimitiveOrWrapper(Class<?> clazz) {
        return clazz.isPrimitive() ||
                clazz.equals(String.class) ||
                clazz.equals(Boolean.class) ||
                clazz.equals(Integer.class) ||
                clazz.equals(Character.class) ||
                clazz.equals(Byte.class) ||
                clazz.equals(Short.class) ||
                clazz.equals(Double.class) ||
                clazz.equals(Long.class) ||
                clazz.equals(Float.class);
    }

/**
     * 共通脱敏
     *
     * @param commonStr
     * @return
     */
    public static String commonStr(String commonStr) {
        if (StrUtil.isBlank(commonStr)) {
            return "";
        } else if (commonStr.length() == 11) {
            return StrUtil.hide(commonStr, 3, commonStr.length() - 4);
        } else if (commonStr.length() == 15 || commonStr.length() == 18) {
            return StrUtil.hide(commonStr, 2, commonStr.length() - 3);
        } else if (commonStr.length() == 1) {
            return StrUtil.hide(commonStr, 0, commonStr.length());
        }else if (commonStr.length() <= 3) {
            return StrUtil.hide(commonStr, 1, commonStr.length());
        } else if (commonStr.length() > 3 && commonStr.length() <= 6) {
            return StrUtil.hide(commonStr, 1, commonStr.length() - 1);
        } else if (commonStr.length() > 6 && commonStr.length() <= 10) {
            return StrUtil.hide(commonStr, 2, commonStr.length() - 2);
        } else if (commonStr.length() > 10 && commonStr.length() <= 16) {
            return StrUtil.hide(commonStr, 3, commonStr.length() - 4);
        } else if (commonStr.length() > 16) {
            return StrUtil.hide(commonStr, 4, commonStr.length() - 5);
        } else {
            return StrUtil.hide(commonStr, 1, commonStr.length());
        }
    

可能这里gpt会建议你用@Pointcut("execution(public * com.xx.aop..*.get*(..))")这种方式拦截,这种我试了,拦截不住。猜测在mvc返回的时候,已经不被aop拦住了,除非手动调用。并且get方式还要user成为bean,不值当。直接拦截controller包吧。

2.Jackson

自定义序列化类



import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

import java.io.IOException;

public class DesensitizeSerializer extends JsonSerializer<String>
 implements ContextualSerializer {


    private SensitiveType type;

    private int startInclude;

    private int endExclude;

    public DesensitizeSerializer() {
        this.type = SensitiveType.COMMON;
    }


    public DesensitizeSerializer(SensitiveType type) {
        this.type = type;
    }


    @Override
    public void serialize(String value, JsonGenerator gen, 
SerializerProvider serializers) throws IOException, IOException {
                switch (type) {
            case COMMON:
                gen.writeString(MsgDesensitizedUtil.commonStr(value));
                break;
            case ID_CARD:
                gen.writeString(MsgDesensitizedUtil.idCardNum(value));
                break;
            case PHONE_NUMBER:
                gen.writeString(MsgDesensitizedUtil.mobilePhone(value));
                break;
            case EMAIL:
                gen.writeString(MsgDesensitizedUtil.email(value));
                break;
            default:
                throw new RuntimeException("未知脱敏类型");
        }
    }


    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
        if (property != null) {
            SensitiveData annotation = property.getAnnotation(SensitiveData.class);
            if (annotation != null) {
                this.type = annotation.value();
            }
        }
        return this;
    }

}

定义针对多种类型的脱敏枚举类

在实体字段上添加注解

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐