实战:springboot之api返回数据脱敏和Logback日志脱敏

331 篇文章 1 订阅
321 篇文章 1 订阅

1. 概叙

数据脱敏是指对敏感数据进行部分或全部掩盖,以保护数据隐私。在实际应用中,我们常常需要在日志、API响应或者数据库中对敏感数据进行脱敏处理,比如身份证号、手机号、邮箱地址等。Spring Boot提供了强大的框架支持,使得我们可以轻松地实现数据脱敏。

下面是日志脱敏和api返回数据脱敏的效果

2. 数据脱敏的场景

常见的数据脱敏场景包括:

  1. 日志记录:防止敏感信息在日志中泄露。
  2. API响应:保护用户隐私,防止敏感信息暴露给客户端。
  3. 数据库存储:在存储之前对数据进行脱敏处理,以确保数据安全。

3.脱敏操作

3.1 定义脱敏注解和枚举

枚举

package com.zxx.study.web.common.myenum;

/**
 * 脱敏类型枚举
 * @author zhouxx
 * @create 2024-07-30 20:26
 */
public enum SensitiveType {

    /**
     * 自定义
     */
    CUSTOMER,
    /**
     * 名称
     **/
    CHINESE_NAME,
    /**
     * 身份证证件号
     **/
    ID_CARD_NUM,
    /**
     * 手机号
     **/
    MOBILE_PHONE,
    /**
     * 固定电话
     */
    FIXED_PHONE,
    /**
     * 密码
     **/
    PASSWORD,
    /**
     * 银行卡号
     */
    BANKCARD,
    /**
     * 邮箱
     */
    EMAIL,
    /**
     * 地址
     */
    ADDRESS,

}

3.2 定义脱敏处理工具类DesensitizedUtil

package com.zxx.study.web.util;

import com.zxx.study.web.annotation.Sensitive;
import com.zxx.study.web.common.myenum.SensitiveType;
import com.zxx.study.web.exception.BaseResultError;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.util.*;
/**
 * @author zhouxx
 * @create 2024-07-30 20:29
 */
@Slf4j
public class DesensitizedUtil<T> {

    /**
     * 脱敏数据列表
     */
    private List<T> list;

    /**
     * 注解列表
     */
    private List<Object[]> fields;

    /**
     * 实体对象
     */
    public Class<T> clazz;


    public DesensitizedUtil(Class<T> clazz) {
        this.clazz = clazz;
    }

    /**
     * 初始化数据
     *
     * @param list 需要处理数据
     */
    public void init(List<T> list) {
        if (list == null) {
            list = new ArrayList<T>();
        }
        this.list = list;

        // 得到所有定义字段
        createSensitiveField();
    }

    /**
     * 初始化数据
     *
     * @param t 需要处理数据
     */
    public void init(T t) {

        list = new ArrayList<T>();

        if (t != null) {
            list.add(t);
        }

        // 得到所有定义字段
        createSensitiveField();
    }

    /**
     * 得到所有定义字段
     */
    private void createSensitiveField() {
        this.fields = new ArrayList<Object[]>();
        List<Field> tempFields = new ArrayList<>();
        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
        for (Field field : tempFields) {
            // 单注解
            if (field.isAnnotationPresent(Sensitive.class)) {
                putToField(field, field.getAnnotation(Sensitive.class));
            }

            // 多注解
//            if (field.isAnnotationPresent(Excels.class))
//            {
//                Excels attrs = field.getAnnotation(Excels.class);
//                Excel[] excels = attrs.value();
//                for (Excel excel : excels)
//                {
//                    putToField(field, excel);
//                }
//            }
        }
    }

    /**
     * 对list数据源将其里面的数据进行脱敏处理
     *
     * @param list
     * @return 结果
     */
    public ApiResult desensitizedList(List<T> list) {

        if (list == null) {
            return ApiResult.fail(BaseResultError.API_Sensitive_NULL);
        }

        // 初始化数据
        this.init(list);

        int failTimes = 0;

        for (T t : this.list) {
            if (desensitization(t).getStatus() != BaseResultError.API_Sensitive_OK.getCode()) {
                failTimes++;
            }
        }

        if (failTimes > 0) {
            return ApiResult.fail(BaseResultError.API_Sensitive_Fail);
        }
        // BaseResultError.API_Sensitive_OK
        return ApiResult.success(list);
    }

    /**
     * 放到字段集合中
     */
    private void putToField(Field field, Sensitive attr) {
        if (attr != null) {
            this.fields.add(new Object[]{field, attr});
        }
    }

    /**
     * 脱敏:JavaBean模式脱敏
     *
     * @param t 需要脱敏的对象
     * @return
     */
    public ApiResult desensitization(T t) {
        if (t == null) {
            return ApiResult.fail(BaseResultError.API_Sensitive_NULL);
        }

        // 初始化数据
        init(t);

        try {
            // 遍历处理需要进行 脱敏的字段
            for (Object[] os : fields) {
                Field field = (Field) os[0];
                Sensitive sensitive = (Sensitive) os[1];
                // 设置实体类私有属性可访问
                field.setAccessible(true);
                desensitizeField(sensitive, t, field);
            }
            return ApiResult.success(t);
        } catch (Exception e) {
//            e.printStackTrace();
            log.error("日志脱敏处理失败,回滚,详细信息:[{}]", e);
            return ApiResult.fail(BaseResultError.API_Sensitive_Fail_Result);
        }
    }

    /**
     * 对类的属性进行脱敏
     *
     * @param attr  脱敏参数
     * @param vo    脱敏对象
     * @param field 脱敏属性
     * @return
     */
    private void desensitizeField(Sensitive attr, T vo, Field field) throws IllegalAccessException {

        if (attr == null || vo == null || field == null) {
            return;
        }

        // 读取对象中的属性
        Object value = field.get(vo);
        SensitiveType sensitiveType = attr.type();
        int prefixNoMaskLen = attr.prefixNoMaskLen();
        int suffixNoMaskLen = attr.suffixNoMaskLen();
        String symbol = attr.symbol();

        //获取属性后现在默认处理的是String类型,其他类型数据可扩展
        Object val = convertByType(sensitiveType, value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        field.set(vo, val);
    }

    /**
     * 以类的属性的get方法方法形式获取值
     *
     * @param o    对象
     * @param name 属性名
     * @return value
     * @throws Exception
     */
    private Object getValue(Object o, String name) throws Exception {
        if ((o != null) && (name != null && !StringUtils.isBlank(name))) {
            Class<?> clazz = o.getClass();
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            o = field.get(o);
        }
        return o;
    }

    /**
     * 根据不同注解类型处理不同字段
     */
    private Object convertByType(SensitiveType sensitiveType, Object value, int prefixNoMaskLen, int suffixNoMaskLen, String symbol) {
        switch (sensitiveType) {
            case CUSTOMER:
                value = customer(value, prefixNoMaskLen, suffixNoMaskLen, symbol);
                break;
            case CHINESE_NAME:
                value = chineseName(value, symbol);
                break;
            case ID_CARD_NUM:
                value = idCardNum(value, symbol);
                break;
            case MOBILE_PHONE:
                value = mobilePhone(value, symbol);
                break;
            case FIXED_PHONE:
                value = fixedPhone(value, symbol);
                break;
            case PASSWORD:
                value = password(value, symbol);
                break;
            case BANKCARD:
                value = bankCard(value, symbol);
                break;
            case EMAIL:
                value = email(value, symbol);
                break;
            case ADDRESS:
                value = address(value, symbol);
                break;
        }
        return value;
    }

    /*--------------------------下面的脱敏工具类也可以单独对某一个字段进行使用-------------------------*/

    /**
     * 【自定义】 根据设置进行配置
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public Object customer(Object value, int prefixNoMaskLen, int suffixNoMaskLen, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }

        return value;
    }

    /**
     * 对字符串进行脱敏处理
     *
     * @param s               需处理数据
     * @param prefixNoMaskLen 开头展示字符长度
     * @param suffixNoMaskLen 结尾展示字符长度
     * @param symbol          填充字符
     * @return
     */
    private String handleString(String s, int prefixNoMaskLen, int suffixNoMaskLen, String symbol) {
        // 是否为空
        if (StringUtils.isBlank(s)) {
            return "";
        }

        // 如果设置为空之类使用 * 代替
        if (StringUtils.isBlank(symbol)) {
            symbol = "*";
        }

        // 对长度进行判断
        int length = s.length();
        if (length > prefixNoMaskLen + suffixNoMaskLen) {
            String namePrefix = StringUtils.left(s, prefixNoMaskLen);
            String nameSuffix = StringUtils.right(s, suffixNoMaskLen);
            s = StringUtils.rightPad(namePrefix, StringUtils.length(s) - suffixNoMaskLen, symbol).concat(nameSuffix);
        }

        return s;
    }

    /**
     * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String chineseName(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示一个字符
            int prefixNoMaskLen = 1;
            int suffixNoMaskLen = 0;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";
    }

    /**
     * 【身份证号】显示最后四位,其他隐藏。共计18位或者15位,比如:*************1234
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String idCardNum(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 结尾只展示四个字符
            int prefixNoMaskLen = 0;
            int suffixNoMaskLen = 4;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";
    }

    /**
     * 【固定电话】 显示后四位,其他隐藏,比如:*******3241
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String fixedPhone(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 结尾只展示四个字符
            int prefixNoMaskLen = 0;
            int suffixNoMaskLen = 4;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";

    }

    /**
     * 【手机号码】前三位,后四位,其他隐藏,比如:135****6810
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String mobilePhone(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示三个字符  结尾只展示四个字符
            int prefixNoMaskLen = 3;
            int suffixNoMaskLen = 4;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";
    }

    /**
     * 【地址】只显示到地区,不显示详细地址,比如:湖南省长沙市岳麓区***
     * 只能处理 省市区的数据
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return
     */
    public String address(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示九个字符
            int prefixNoMaskLen = 9;
            int suffixNoMaskLen = 0;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";
    }

    /**
     * 【电子邮箱】 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String email(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示一个字符  结尾只展示@及后面的地址
            int prefixNoMaskLen = 1;
            int suffixNoMaskLen = 4;

            String s = (String) value;
            if (StringUtils.isBlank(s)) {
                return "";
            }

            // 获取最后一个@
            int lastIndex = StringUtils.lastIndexOf(s, "@");
            if (lastIndex <= 1) {
                return s;
            } else {
                suffixNoMaskLen = s.length() - lastIndex;
            }

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";

    }

    /**
     * 【银行卡号】前六位,后四位,其他用星号隐藏每位1个星号,比如:6222600**********1234
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return 脱敏后数据
     */
    public String bankCard(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示六个字符  结尾只展示四个字符
            int prefixNoMaskLen = 6;
            int suffixNoMaskLen = 4;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";

    }

    /**
     * 【密码】密码的全部字符都用*代替,比如:******
     *
     * @param value  需处理数据
     * @param symbol 填充字符
     * @return
     */
    public String password(Object value, String symbol) {

        //针对字符串的处理
        if (value instanceof String) {
            // 对前后长度进行设置 默认 开头只展示六个字符  结尾只展示四个字符
            int prefixNoMaskLen = 0;
            int suffixNoMaskLen = 0;

            return handleString((String) value, prefixNoMaskLen, suffixNoMaskLen, symbol);
        }
        return "";
    }
}

3.3 自定义脱敏对象UserSensitiveVo

/**
 * @author zhouxx
 * @create 2024-07-30 20:56
 */
@Data
@Builder
public class UserSensitiveVo {
    private static final long serialVersionUID = 1L;

    /** 普通用户ID */
    private Long userId;

    /** 昵称 */
    @Sensitive(type = SensitiveType.CUSTOMER,prefixNoMaskLen = 2,suffixNoMaskLen = 1)
    private String nickName;

    /** 姓名 */
    @Sensitive(type = SensitiveType.CHINESE_NAME)
    private String userName;

    /** 身份证 */
    @Sensitive(type = SensitiveType.ID_CARD_NUM)
    private String identityCard;

    /** 手机号码 */
    @Sensitive(type = SensitiveType.MOBILE_PHONE)
    private String phoneNumber;

}

3.4 自定义api返回数据脱敏

入口方法

    @SneakyThrows
    @GetMapping("/user")
    public ApiResult user(@RequestParam(value ="name", required = false) String name)  {

        // 脱敏对象
        UserSensitiveVo user = UserSensitiveVo.builder()
                .userId(100L)
                .userName("zhouxx")
                .nickName("周星星")
                .phoneNumber("1322341234")
                .identityCard("330320194910013321").build();
        DesensitizedUtil<UserSensitiveVo> desensitizedUtils = new DesensitizedUtil<>(UserSensitiveVo.class);
        log.info("{}",user);
        return desensitizedUtils.desensitization(user);
    }

    @SneakyThrows
    @GetMapping("/users")
    public ApiResult users(@RequestParam(value ="name", required = false) String name)  {

        //脱敏队列
        List<UserSensitiveVo> users = new ArrayList<UserSensitiveVo>();
        for(int i=0;i<5;i++){
            UserSensitiveVo user = UserSensitiveVo.builder()
                    .userId(100L+i)
                    .userName("zhouxx"+i)
                    .nickName("周星星"+i)
                    .phoneNumber("13"+i+"2341234")
                    .identityCard("3"+i+"03201949100133"+i+"1").build();
            users.add(user);
        }
        DesensitizedUtil<UserSensitiveVo> desensitizedUtils = new DesensitizedUtil<>(UserSensitiveVo.class);
        log.info("{}",users);
        return desensitizedUtils.desensitizedList(users);

    }

效果

3.5 自定义api返回数据脱敏AOP切面统一处理

切入点注解Encryption

入口添加注解,不用每个方法都增加脱敏处理逻辑

切面EncryptAspect: 其实就是把“3.4 controller”中逐个处理的逻辑,移到aop,统一规范处理,减少重复代码。

脱敏效果

3.6日志脱敏:重写AbstractMatcherFilter的decide方法

在 logback.xml 中配置过滤器:

3.7日志脱敏: 使用 Logstash 的 Mutate 插件(可以自行配置)

Logstash 是一款开源的数据处理引擎,可以用于收集、处理、转换和输出日志等数据。Logstash 中的 Mutate 插件提供了多个常用的数据处理功能,包括脱敏。

比如,使用 Logstash 的 mutate 插件,可以对手机号码进行脱敏处理:

filter {
  mutate {
    gsub => [
      "message", "(?<![\\d])[1][3-9]\\d{9}(?![\\d])", "****"
    ]
  }
}

上述配置会将日志中所有的手机号码替换为 ****。

依赖包

logback-desensitize.yml配置说明

log.info("your email:{}, your phone:{}", "123456789@qq.com","15310763497");
log.info("your email={}, your cellphone={}", "123456789@qq.com","15310763497");
# 日志脱敏
log-desensitize:
  # 是否忽略大小写匹配,默认为true
  ignore: true
  # 是否开启脱敏,默认为false
  open: true
  # pattern下的key/value为固定脱敏规则
  pattern:
    # 邮箱 - @前第4-7位脱敏
    email: "@>(4,7)"
    # qq邮箱 - @后1-3位脱敏
    qqemail: "@<(1,3)"
    # 姓名 - 姓脱敏,如*杰伦
    name: 1,1
    # 密码 - 所有需要完全脱敏的都可以使用内置的password
    password: password
  patterns:
    # 身份证号,key后面的字段都可以匹配以下规则(用逗号分隔)
    - key: identity,idcard
      # 定义规则的标识
      custom:
        # defaultRegex表示使用组件内置的规则:identity表示身份证号 - 内置的18/15位
        - defaultRegex: identity
          position: 9,13
        # 内置的other表示如果其他规则都无法匹配到,则按该规则处理
        - defaultRegex: other
          position: 9,10
    # 电话号码,key后面的字段都可以匹配以下规则(用逗号分隔)
    - key: phone,cellphone,mobile
      custom:
        # 手机号 - 内置的11位手机匹配规则
        - defaultRegex: phone
          position: 4,7
        # 自定义正则匹配表达式:座机号(带区号,号码七位|八位)
        - customRegex: "^0[0-9]{2,3}-[0-9]{7,8}"
        # -后面的1-4位脱敏
          position: "-<(1,4)"
        # 自定义正则匹配表达式:座机号(不带区号)
        - customRegex: "^[0-9]{7,8}"
          position: 3,5
        # 内置的other表示如果其他规则都无法匹配到,则按该规则处理
        - defaultRegex: other
          position: 1,3
    # 这种方式不太推荐 - 一旦匹配不上,就不会脱敏
    - key: localMobile
      custom:
          customRegex: "^0[0-9]{2,3}-[0-9]{7,8}"
          position: 1,3

使用 AOP 实现日志脱敏
在 Spring Boot 中,我们还可以使用 AOP(面向切面编程)的方式,在日志输出前对敏感信息进行脱敏处理。

比如,我们可以定义一个 LogAspect 切面类,在 @Before 注解的方法中,对方法参数中的敏感信息进行脱敏处理,然后再调用目标方法。

@Aspect
@Component
public class LogAspect {

    private static final String PHONE_PATTERN = "(?<![\\d])[1][3-9]\\d{9}(?![\\d])";

    @Before("execution(* com.example.controller..*(..))")
    public void beforeController(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args == null) {
            return;
        }
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof String) {
                String msg = (String) args[i];
                Pattern pattern = Pattern.compile(PHONE_PATTERN);
                Matcher matcher = pattern.matcher(msg);
                StringBuffer sb = new StringBuffer();
                while (matcher.find()) {
                    String phone = matcher.group();
                    matcher.appendReplacement(sb, phone.substring(0, 3) + "****" + phone.substring(7));
                }
                matcher.appendTail(sb);
                args[i] = sb.toString();
            }
        }
        try {
            joinPoint.proceed(args);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

上面的切面类会在所有 com.example.controller 包下的方法调用前执行,对方法参数中的手机号码进行脱敏处理,然后再调用目标方法。

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-无-为-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值