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


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

1. 思路梳理

脱敏处理的三种方式:

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

面向切面编程思想实现:

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

2. 逻辑实现

2.1 脱敏的标识注解

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

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

2.2 需要被切面增强的位置处理

@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类中字段上。

2.3 切面实现逻辑

@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());
    }
}

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

3. 源代码

Github:https://github.com/itcrud/itcrud-commons(logaspecthandler)


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
xshell、CRT上使用vbscript更高效连接定位到服务器以及目录、数据库 xshell、CRT上使用vbscript更高效连接定位到服务器以及目录、数据库
这篇文章分享一个好用的脚本,用在xshell和CRT上,真的很爽,也是简单的不要不要的。当负责的项目有多个环境,看日志需要到Linux环境下,数据库也不能用navicat等工具连接,这个时候只能使用xshell或者CRT进入对应的机器,但是这里存在的麻烦就是机器太多(测试环境、演示环境、生产环境),每次都要用ssh命令在不同的……
2018-12-15
下一篇 
nginx基础学习(一):linux环境下nginx的安装和配置文件的初步认识 nginx基础学习(一):linux环境下nginx的安装和配置文件的初步认识
nginx目前在市场上使用是非常广泛的,作为一个开发人员,可以不会nginx的高级使用,但是基本的使用场景以及日常问题排查技能还是要有的。nginx使用的方向主要有三个,分别是路由功能、负载均衡功能以及静态资源服务功能。路由功能主要是用来将不同请求分发到不用的服务上,负载均衡是将请求分发到一个服务的集群中的不同虚拟机上,这两种……
2018-12-09
  目录