每一年都奔走在自己热爱里 没有人是一座孤岛,总有谁爱着你

岁
万
月
12

基于 注解 + AOP 的脱敏方案

一:定义脱敏类型枚举

定义需要脱敏的数据类型,如姓名、身份证号、手机号等。

public enum SensitiveType {
    NAME,       // 姓名
    ID_CARD,    // 身份证
    MOBILE,     // 手机号
    AMOUNT      // 金额
} 

二 :定义脱敏注解

可通过 conditionField + 固定值,或者 condition 表达式两种方式指定条件。
条件字段必须存在于同一实体类中,且可访问。

package org.beizhilib.common.aspect;
import org.beizhilib.common.enums.SensitiveType;
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitize {
    /**
     * 脱敏类型
     */
    SensitiveType type();

    /**
     * 条件字段名(用于判断是否满足脱敏条件,如 status)
     */
    String conditionField() default "";

    /**
     * 条件期望值(如 "完结")
     * 支持 SpEL 表达式,例如 "#status == '完结'"
     */
    String condition() default "";
}

三:脱敏工具类

package org.beizhilib.common.utils;

import cn.hutool.core.util.DesensitizedUtil;
import org.beizhilib.common.enums.SensitiveType;

import java.math.BigDecimal;

/**
 * 脱敏工具类
 *
 * @author Bryson
 * @date 2026/3/19
 */
public class DesensitizeUtils {

    public static String desensitize(String value, SensitiveType type) {
        if (value == null) return null;
        switch (type) {
            case NAME:
                return DesensitizedUtil.desensitized(value,DesensitizedUtil.DesensitizedType.CHINESE_NAME);
            case ID_CARD:
                return DesensitizedUtil.desensitized(value,DesensitizedUtil.DesensitizedType.ID_CARD);
            case MOBILE:
                return DesensitizedUtil.desensitized(value,DesensitizedUtil.DesensitizedType.MOBILE_PHONE);
            case AMOUNT:
                return desensitize(value);
            default:
                return value;
        }
    }

    public static String desensitize(String value) {
        if (value == null) return null;
        // 金额脱敏规则(可自定义)
        return "***";
    }
}

四:AOP 切面实现

package org.beizhilib.common.aspect;

import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.beizhilib.common.enums.SensitiveType;
import org.beizhilib.common.utils.DesensitizeUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Collection;

/**
 * TODO 类作用描述
 *
 * @author Bryson
 * @date 2026/3/19
 */
@Aspect
@Component
@Slf4j
public class DesensitizeAspect {
    /**
     * 拦截 Service 方法(可自定义切点)
     */
    @Around("execution(* org.beizhilib.biz.service.impl.SysUserServiceImpl.selectPage(..))")
    public Object doDesensitize(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed(); // 先执行原方法获取返回值
        if (result == null) return null;

        // 对返回值进行脱敏处理
        processObject(result);
        return result;
    }

    /**
     * 递归处理对象(支持集合、数组、分页对象等)
     */
    private void processObject(Object obj) {
        if (obj == null) return;

        // 处理集合
        if (obj instanceof Collection) {
            ((Collection<?>) obj).forEach(this::processObject);
            return;
        }

        // 处理数组
        if (obj.getClass().isArray()) {
            for (Object item : (Object[]) obj) {
                processObject(item);
            }
            return;
        }

        // 处理分页对象(MyBatis-Plus Page)
        if (obj instanceof com.baomidou.mybatisplus.extension.plugins.pagination.Page) {
            ((com.baomidou.mybatisplus.extension.plugins.pagination.Page<?>) obj).getRecords()
                    .forEach(this::processObject);
            return;
        }

        // 处理分页对象(PageHelper PageInfo<?>)
        if (obj instanceof PageInfo<?>) {
            ((PageInfo<?>) obj).getList()
                    .forEach(this::processObject);
            return;
        }

        // 处理普通 Java 对象
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            Desensitize annotation = field.getAnnotation(Desensitize.class);
            if (annotation == null) continue;

            field.setAccessible(true);
            try {
                Object fieldValue = field.get(obj);
                if (fieldValue == null) continue;

                // 判断是否满足脱敏条件
                boolean shouldDesensitize = evaluateCondition(obj, field, annotation);
                if (!shouldDesensitize) continue;

                // 执行脱敏
                Object desensitizedValue = doDesensitizeValue(fieldValue, annotation.type());
                field.set(obj, desensitizedValue);
            } catch (IllegalAccessException e) {
                // log error
            }
        }
    }

    /**
     * 解析条件表达式
     */
    private boolean evaluateCondition(Object target, Field field, Desensitize annotation) {
        String condition = annotation.condition();
        if (condition.isEmpty()) {
            // 无条件,默认脱敏
            return true;
        }

        try {
            EvaluationContext context = new StandardEvaluationContext(target);
            ExpressionParser parser = new SpelExpressionParser();
            Boolean result = parser.parseExpression(condition).getValue(context, Boolean.class);
            return result != null && result;
        } catch (Exception e) {
            log.error("SpEL 评估失败: condition={}", condition, e);
            // log error,默认不脱敏
            return false;
        }
    }

    /**
     * 根据类型脱敏
     */
    private Object doDesensitizeValue(Object value, SensitiveType type) {
        if (value instanceof String) {
            return DesensitizeUtils.desensitize((String) value, type);
        }
        return value;
    }
}

五:配置启用 AOP

在 Spring Boot 启动类上添加 @EnableAspectJAutoProxy 注解

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

六:实体类中使用注解

    /**
     * 用户昵称
     */
    @Desensitize(type = SensitiveType.NAME, condition = "remark == '完结'")
    private String nickName;

    /**
     * 手机号码
     */
    @Desensitize(type = SensitiveType.MOBILE, condition = "remark == '完结'")
    private String phonenumber;

    /**
     * 备注
     */
    private String remark;

七:使用示例

    @Override
    public PageInfo<SysUser> selectPage(UserDto user) {
        PageHelper.startPage(user.getPageIndex(), user.getPageSize());
        List<SysUser> list = sysUserMapper.selectAll(user);
        if (list == null) {
            return null;
        }
        return new PageInfo<>(list);
    }

当 Controller 调用 SysUserService.selectPage 返回的用户列表时,AOP 会自动对其中满足条件( remark == '完结')的字段进行脱敏,业务代码完全无需关心脱敏逻辑。

posted @ 2026-03-19 14:10  果咩那塞  阅读(13)  评论(0)    收藏  举报