项目中手机、姓名、身份证信息等在日志和响应数据中脱敏操作

项目日志打印请求的入参和出参,用来跟踪数据信息,方便根据日志信息排查问题,在涉及到用户敏感信息的时候,为了安全的考虑,不能直接将这些信息直接输出到日志文件中,需要做脱敏操作。如果这个脱敏操作放在项目的业务代码中,只要出现需要脱敏的信息就进行一次脱敏操作,这样会有很多脱敏的代码冗余;如果将脱敏的逻辑代码提出来,在需要脱敏的位置调用此段逻辑代码,也会比较麻烦,有可能会造成漏掉的问题,写起来也是很麻烦。这里最好的方法莫过于使用切面,通过对请求的出参入参进行切入,并将敏感信息做脱敏操作后输入到日志文件中。这样的好处很多,以后新加接口无需考虑脱敏的逻辑,只要使用对应的注解标注需要脱敏的字段,一些需要脱敏的操作交给切面去完成即可。

思路梳理
脱敏处理的三种方式:

直接在业务代码中写入脱敏的逻辑代码,存在代码冗余、易漏写、不易维护等问题;
将脱敏代码作为公共的代码提取出来,在需要脱敏的位置调用脱敏逻辑代码,存在易漏写以及不易维护的问题;
利用面向切面编程思想,对出参和入参的位置使用切面切入,在切面中实现对出参和入参的脱敏操作。
面向切面编程思想实现:

使用Around切面,在方法的前部做请求参数的脱敏,这个时候需要将原请求参数做一次拷贝,对拷贝后的入参参数进行脱敏操作,输出到日志中,这样不会影响到传入服务层的数据正确性;
在方法的后部做出参的脱敏,如果出参在外部显示不需要脱敏,则要拷贝后做脱敏,在日志中输入,反之则直接在出参上做脱敏即可,完成脱敏操作,输出到日志中;
在需要脱敏的字段上加上指定的注解,这个注解可以自定义,主要是用来标识需要脱敏的字段。
逻辑实现
脱敏的标识注解

@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveWord {
}

@Target指定该注解只能加在字段和请求的参数上,用来表示需要脱敏的字段或者参数。一般POST请求都是直接传入到对象接收,这个时候将注解添加到对象内需要脱敏的字段上。如果是GET请求,参数直接接收的比较多,可以将此直接加在参数上。(下面Controller有示例)需要被切面增强的位置处理

@Controller
@Slf4j
@RequestMapping("/itcrud")
public class LogAspectController {

    //GET请求
    @GetMapping("/aspectLogHandlerForGet")
    @ResponseBody
    public LogAspectHandlerVO aspectLogHandlerForGet(@RequestParam("phone") @SensitiveWord String phone,@Param("name") String name) {
        //……
        return vo;
    }

    //POST请求
    @PostMapping("/aspectLogHandlerForPost")
    @ResponseBody
    public LogAspectHandlerVO aspectLogHandlerForPost(@RequestBody @SensitiveWord LogAspectHandlerReqDTO reqDTO) {
        //……
        return vo;
    }
}

GET请求,直接将@SensitiveWord加在参数上。

POSY请求,将@SensitiveWord加在ReqDTO类中字段上。切面实现逻辑

@Aspect
@Component
@Slf4j
public class LogAspectHandler {

    @Pointcut("execution(* com.itcrud.common.web..*Controller.*(..))")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //请求方法信息
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method method = ms.getMethod();
        StringBuilder logStr = new StringBuilder();
        logStr.append("请求方法:").append(joinPoint.getTarget().getClass().getName())
                .append(".").append(method.getName()).append("()");
        //获取参数,组装参数
        Parameter[] parameters = method.getParameters();
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ms.getParameterNames();
        StringBuilder params = new StringBuilder(" 请求参数:");
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            Object value = args[i];
            SensitiveWord sw = parameter.getAnnotation(SensitiveWord.class);
            Object copyValue = null;
            if (sw != null) {//需要脱敏
                if (value instanceof String) {
                    value = sensitive(value);
                } else {
                    //如果是对象的话需要考虑对象内套用对象的问题,也就涉及到递归,这里不具体实现,此处简单举例
                    //对象创建副本,在副本上操作
                    copyValue = value.getClass().newInstance();
                    BeanUtils.copyProperties(value, copyValue);
                    requestParamSensitive(copyValue);
                }
            } else {
                //对象创建副本,在副本上操作
                int modifiers = value.getClass().getModifiers();
                if (!Modifier.isFinal(modifiers)) {
                    copyValue = value.getClass().newInstance();
                    BeanUtils.copyProperties(value, copyValue);
                    requestParamSensitive(copyValue);
                }
            }
            params.append(parameterNames[i]).append(":").append(copyValue == null
                    ? JSON.toJSONString(value) : JSON.toJSONString(copyValue)).append(";");
        }
        //执行操作
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = joinPoint.proceed();
        stopWatch.stop();
        /*一般controller都是统一一个VO响应类封装响应信息,
        然后里面用data字段来封装具体的数据,这个时候可以根据实际情况来获取具体数据信息进行脱敏操作,
        这里仅做示例*/
        if (result instanceof LogAspectHandlerVO) {
            reflectFields(result);
        }
        logStr.append(params.toString());
        logStr.append(" 执行耗时:").append(stopWatch.getTotalTimeMillis()).append("ms");
        logStr.append(" 响应数据:").append(JSON.toJSONString(result));
        log.info(logStr.toString());
        return result;
    }

    //请求参数处理部分
    private void requestParamSensitive(Object value) throws Exception {
        if (value instanceof List) {
            //TODO dosomthing
        }
        if (value instanceof Map) {
            //TODO doSomthing
        }
        if (value instanceof LogAspectHandlerReqDTO) {
            reflectFields(value);
        }
    }

    //反射类中字段
    private void reflectFields(Object object) throws Exception {
        Class voClazz = object.getClass();
        Field[] fields = voClazz.getDeclaredFields();
        if (fields != null && fields.length != 0) {
            for (Field field : fields) {
                SensitiveWord sw = field.getAnnotation(SensitiveWord.class);
                if (sw != null) {
                    field.setAccessible(true);
                    Object f = field.get(object);
                    if (f instanceof String) field.set(object, sensitive(f));
                }
            }
        }
    }

    //脱敏操作
    private String sensitive(Object value) {
        if (value == null) return "null";
        String str = String.valueOf(value);
        if (StringUtils.isBlank(str)) return "";
        int length = str.length();
        if (length <= 3) {
            str = str.substring(0, 1) + (length == 3 ? "**" : "*");
        } else {
            int v = length >> 1;
            if (v > 7) {
                str = longSensitive(str, 4);
            } else if (v > 4) {
                str = longSensitive(str, 3);
            } else if (v > 3) {
                str = longSensitive(str, 2);
            } else {
                str = longSensitive(str, 1);
            }
        }
        return str;
    }

    //长敏感词
    private String longSensitive(String str, int offset) {
        String s = str.substring(0, offset);
        for (int i = 0; i < str.length() - (offset << 1); i++) s += "*";
        return s + str.substring(str.length() - offset, str.length());
    }
}

这里的实现不是很全,只是对当前已知的出参和入参处理思路,其实还有很多其他的可能性,这个需要根据使用的项目做具体的调整。(比如出参和入参中存在集合,集合中有对象,对象里面可能有集合的字段属性等等,都是要纳入考虑范围之内的,这里就不具体去写了,有兴趣的可以根据自己的项目做相应的完善)

Code
码云(gitee):https://gitee.com/itcrud/itcrud-commons(logaspecthandler)
————————————————
版权声明:本文为CSDN博主「程序猿洞晓」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/millery22/article/details/123566699

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值