SpringBoot动态脱敏实战:从注解到AOP的优雅打码术

前言

最近在做项目时遇到一个头疼的问题:业务方要求对用户敏感信息进行脱敏处理,比如手机号、身份证、邮箱等。如果手动处理,不仅代码冗余,还容易遗漏。有没有一种优雅的解决方案呢?

答案是肯定的!今天就来分享一套基于SpringBoot + 注解 + AOP的动态脱敏方案,让你的敏感数据自动"戴上面具"。

为什么要做数据脱敏?

在互联网时代,数据安全和隐私保护越来越重要。无论是响应《数据安全法》还是满足业务需求,数据脱敏都成了开发者的必修课。

想象一下,如果用户的真实手机号、身份证号直接暴露在日志或前端页面中,那后果不堪设想。数据脱敏就像给敏感信息穿上"隐身衣",既能保证业务正常流转,又能保护用户隐私。

核心设计思路

我们的目标是实现一个"无侵入"的数据脱敏方案,即:

  1. 业务代码无需感知脱敏逻辑
  2. 通过注解标记需要脱敏的字段
  3. AOP自动拦截并处理返回数据

这种设计的核心思想是"关注点分离",让业务逻辑专注于业务本身,脱敏逻辑交给框架自动处理。

实现方案详解

1. 定义脱敏类型枚举

首先,我们需要定义常见的脱敏类型:

public enum SensitiveTypeEnum {
    /**
     * 中文姓名脱敏(保留姓氏,其余用*代替)
     * 示例:张三 -> 张*
     */
    CHINESE_NAME(1, "中文姓名"),

    /**
     * 身份证号码脱敏(保留前3位和后3位)
     * 示例:110101199003071234 -> 110****1234
     */
    ID_CARD(2, "身份证号码"),

    /**
     * 手机号码脱敏(保留前3位和后4位)
     * 示例:13812345678 -> 138****5678
     */
    PHONE(3, "手机号码"),

    // ... 其他类型
}

2. 创建脱敏注解

定义一个自定义注解,用于标记需要脱敏的字段:

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
    // 脱敏类型
    SensitiveTypeEnum type() default SensitiveTypeEnum.CUSTOM;
    
    // 前缀保留长度
    int prefixKeep() default 0;
    
    // 后缀保留长度
    int suffixKeep() default 0;
    
    // 替换字符
    String maskChar() default "*";
    
    // 是否启用脱敏
    boolean enabled() default true;
}

3. 实现脱敏处理器

创建脱敏工具类,实现各种脱敏算法:

public class SensitiveDataHandler {
    public static String mask(String value, SensitiveData sensitiveData) {
        if (value == null || value.isEmpty() || !sensitiveData.enabled()) {
            return value;
        }

        switch (sensitiveData.type()) {
            case CHINESE_NAME:
                return maskChineseName(value);
            case PHONE:
                return maskPhone(value);
            // ... 其他类型处理
            default:
                return value;
        }
    }

    public static String maskPhone(String phone) {
        if (phone == null || phone.length() != 11) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    
    // 其他脱敏方法...
}

4. AOP切面实现

这是整个方案的核心,通过AOP拦截Controller的返回值:

@Aspect
@Order(1)
@Component
public class SensitiveDataAspect {
    @Around("execution(* *(..)) && (@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.RequestMapping))")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 执行原方法
        Object result = point.proceed();
        
        // 对返回结果进行脱敏处理
        if (result != null) {
            result = processSensitiveData(result);
        }
        
        return result;
    }

    private Object processSensitiveData(Object obj) {
        if (obj == null) {
            return obj;
        }

        if (obj instanceof Collection) {
            // 处理集合类型
            Collection<?> collection = (Collection<?>) obj;
            for (Object item : collection) {
                processSensitiveData(item);
            }
        } else if (obj instanceof Map) {
            // 处理Map类型
            Map<?, ?> map = (Map<?, ?>) obj;
            for (Object value : map.values()) {
                processSensitiveData(value);
            }
        } else {
            // 处理普通对象
            processObjectSensitiveData(obj);
        }

        return obj;
    }

    private void processObjectSensitiveData(Object obj) {
        if (obj == null) {
            return;
        }

        Class<?> clazz = obj.getClass();
        
        // 处理当前类及其父类的所有字段
        while (clazz != null && !clazz.equals(Object.class)) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                try {
                    field.setAccessible(true);
                    Object value = field.get(obj);

                    // 检查字段是否有@SensitiveData注解
                    if (field.isAnnotationPresent(SensitiveData.class)) {
                        SensitiveData sensitiveData = field.getAnnotation(SensitiveData.class);
                        
                        if (value instanceof String) {
                            String originalValue = (String) value;
                            String maskedValue = SensitiveDataHandler.mask(originalValue, sensitiveData);
                            field.set(obj, maskedValue);
                        }
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            clazz = clazz.getSuperclass();
        }
    }
}

实际应用示例

定义一个用户信息实体类:

public class UserInfo {
    @SensitiveData(type = SensitiveTypeEnum.CHINESE_NAME)
    private String name;
    
    @SensitiveData(type = SensitiveTypeEnum.PHONE)
    private String phone;
    
    @SensitiveData(type = SensitiveTypeEnum.EMAIL)
    private String email;
    
    // ... getter/setter
}

在Controller中正常使用:

@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/info")
    public UserInfo getUserInfo() {
        return new UserInfo(
            "张三丰",        // 脱敏后:张**
            "13812345678",  // 脱敏后:138****5678
            "zhang@example.com" // 脱敏后:z****g@...
        );
    }
}

方案优势

  1. 无侵入性:业务代码无需感知脱敏逻辑
  2. 灵活配置:支持多种脱敏类型和自定义规则
  3. 性能友好:仅在数据返回时进行脱敏,不影响业务处理
  4. 易于扩展:新增脱敏类型只需扩展枚举和处理方法
  5. 类型安全:编译期即可检查注解使用是否正确

注意事项

  1. 性能考虑:对于高频接口,要考虑脱敏操作的性能开销
  2. 缓存策略:脱敏后的数据通常不应被缓存
  3. 日志处理:确保敏感信息不会在日志中明文输出
  4. 异常处理:脱敏过程中的异常不应影响正常业务流程

总结

通过这套基于注解和AOP的动态脱敏方案,我们可以实现:

  • 业务代码的简洁性:无需在业务逻辑中处理脱敏
  • 配置的灵活性:通过注解参数灵活控制脱敏规则
  • 实现的统一性:一处定义,处处生效

这套方案在实际项目中已经得到了广泛应用,不仅提高了代码质量,也大大降低了数据泄露的风险。希望这个方案能对大家有所帮助!

在数据安全越来越重要的今天,作为开发者,我们有责任保护好用户的数据隐私。这套动态脱敏方案,就是我们为数据安全加上的重要一层防护。

如果你觉得这篇文章对你有帮助,欢迎关注我们的公众号"服务端技术精选",获取更多实用的技术干货!


本文通过SpringBoot + 注解 + AOP实现了一套优雅的数据脱敏方案,让敏感信息自动脱敏,既保证了业务功能的完整性,又提升了数据安全性。


标题:SpringBoot动态脱敏实战:从注解到AOP的优雅打码术
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/06/1767667265344.html

    0 评论
avatar