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