在业务系统中,我们经常会需要记录用户的操作记录,从而方便对用户的行为进行审计。得益于java aop和对注解的支持,我们大多数方案是通过注解的扫描,然后通过aop的方式对操作行为进行拦截并进行记录。大方向上个这个基本是一致的,但是如何实现就看各个团队自己的方案了,针对这个情况,我们抽象出一个统一的解决方案,以期能在基础层面满足大多数场景的要求。
设计思路
注解类
首先,我们要明确,行为审计要记录的是什么?基本可以抽象为XX在XX时候做了XX事情,剩下的就是对这个事情的详细描述,例如操作对象的具体信息,对象前后的变化等。但是,对于不同的资源对象,又需要不同的处理方法,可能是去查询对应的表,或者根据约定的逻辑进行处理。由此,我们可以基本确定注解的属性。
- type 行为类型
- category 行为分类
- action 行为
- description 描述
- handler 处理
处理类
对于不同的业务主体,我们需要对其有不同的处理逻辑,那么就不能简单的在aop处进行处理。这里我们就可以考虑借助spring的依赖注入的特性,在一个Bean中,可以将所有该类型的子类通过集合的形式都注入进来,这样我们就可以在该Bean初始化完成之后,将处理类型的集合准换为以class为key的map。考虑到一些复杂的要求,我们需要对操作前后值都进行记录,所以这里需要一个beforeValues函数和afterValues函数。并且定义一个抽象函数,让实现类去进行处理。
代码实现
OperationAudit 注解类
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface OperationAudit {
/**
* 类型
*
* @return
*/
String type() default "";
/**
* 分类
*
* @return
*/
String category() default "";
/**
* 行为
*
* @return
*/
String action() default "";
/**
* 描述
*
* @return
*/
String description() default "";
/**
* 处理实现类
*
* @return
*/
Class<?> handler() default Void.class;
}
OperationAuditContext 将整个过程中的属性值包装成一个Context对象,传递给处理函数
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class OperationAuditContext {
private Map<String, Object> request;
private Object response;
private Map<String, Object> beforeValues;
private Map<String, Object> afterValues;
private String type;
private String category;
private String action;
private String description;
private Instant startAt;
private Instant endAt;
private Boolean success;
private String errorMsg;
}
OperationAuditHandler 处理抽象类
public abstract class OperationAuditHandler {
public Boolean isDefault() {
return false;
}
public Map<String, Object> beforeValues(Map<String, Object> args) {
return null;
}
public Map<String, Object> afterValues(Map<String, Object> args) {
return null;
}
public abstract void handle(OperationAuditContext operationAuditContext);
}
OperationAuditAspect 切面实现
@RequiredArgsConstructor
@Aspect
public class OperationAuditAspect extends ActEventAspect {
private final List<OperationAuditHandler> handlers;
private Map<Class<?>, OperationAuditHandler> handlerMap;
@PostConstruct
public void init() {
handlerMap = new HashMap<>();
handlers.forEach(process -> handlerMap.put(process.getClass(), process));
}
@Pointcut("@annotation(com.trionesdev.spring.core.audit.OperationAudit)")
public void auditLogAround() {
}
@Around(value = "auditLogAround()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Instant startAt = Instant.now();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
OperationAudit operationAudit = AnnotationUtils.getAnnotation(methodSignature.getMethod(), OperationAudit.class);
OperationAuditHandler handler = Objects.isNull(operationAudit) ? null : getHandler(operationAudit.handler());
if (operationAudit == null || handler == null) {
return joinPoint.proceed();
}
OperationAuditContext operationAuditContext = new OperationAuditContext();
operationAuditContext.setStartAt(startAt);
operationAuditContext.setType(operationAudit.type());
operationAuditContext.setCategory(operationAudit.category());
operationAuditContext.setAction(operationAudit.action());
operationAuditContext.setDescription(operationAudit.description());
operationAuditContext.setRequest(mapArgs(joinPoint, methodSignature));
operationAuditContext.setBeforeValues(handler.beforeValues(operationAuditContext.getRequest()));
Object result;
try {
result = joinPoint.proceed();
operationAuditContext.setResponse(result);
operationAuditContext.setAfterValues(handler.afterValues(operationAuditContext.getRequest()));
operationAuditContext.setSuccess(true);
} catch (Throwable e) {
operationAuditContext.setSuccess(false);
operationAuditContext.setErrorMsg(e.getMessage());
throw e;
} finally {
operationAuditContext.setEndAt(Instant.now());
handler.handle(operationAuditContext);
}
return result;
}
public OperationAuditHandler getHandler(Class<?> clazz) {
if (CollectionUtils.isEmpty(handlers)) {
return null;
}
if (clazz == Void.class) {
if (handlers.stream().filter(OperationAuditHandler::isDefault).count() > 1) {
throw new RuntimeException("multi default process");
}
return handlers.stream().filter(OperationAuditHandler::isDefault).findFirst().orElse(null);
} else {
OperationAuditHandler handler = handlerMap.get(clazz);
if (handler == null) {
throw new RuntimeException(clazz.getName() + " process not found");
}
return handler;
}
}
public Map<String, Object> mapArgs(ProceedingJoinPoint joinPoint, MethodSignature methodSignature) {
Object[] args = joinPoint.getArgs();
String[] parameterNames = methodSignature.getParameterNames();
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
if (args.length > i) {
map.put(parameterNames[i], args[i]);
}
}
return map;
}
}
由代码可见,核心思想就是把函数处理的上下文收集起来,在函数执行完成之后进行统一的处理。
使用示例
根据上面的代码可以知道,我们需要定义一个默认的实现类,作为通用的处理逻辑,如果有特殊处理的,再通过指定的实现类去处理。
默认处理
默认实现类,此处的isDefault 返回值设置为ture。
@RequiredArgsConstructor
@Component
public class DefaultOperationHandler extends OperationAuditHandler {
private final ActorContext actorContext;
private final OperationLogManager operationLogManager;
@Override
public Boolean isDefault() {
return true;
}
@Override
public void handle(OperationAuditContext operationAuditContext) {
OperationLogPO operationLog = OperationLogPO.builder()
.actorId(actorContext.getUserId())
.actorRole(actorContext.getRole())
.type(operationAuditContext.getType())
.category(operationAuditContext.getCategory())
.action(operationAuditContext.getAction())
.description(operationAuditContext.getDescription())
.startAt(operationAuditContext.getStartAt())
.endAt(operationAuditContext.getEndAt())
.success(operationAuditContext.getSuccess())
.errorMsg(StringUtils.substring(operationAuditContext.getErrorMsg(), 0, 500))
.build();
operationLogManager.create(operationLog); //写入数据库
}
}
使用默认处理
@OperationAudit(type = "LOGIN", category = "ORG", action = "LOGIN", description = "用户登录")
指定处理
指定实现类
@RequiredArgsConstructor
@Component
public class LoginOperationHandler extends OperationAuditHandler {
private final JwtFacade jwtFacade;
private final OperationLogProvider operationLogProvider;
@Override
public void handle(OperationAuditContext operationAuditContext) {
OperationLogCreateCmd cmd = OperationLogCreateCmd.builder()
.type(operationAuditContext.getType())
.category(operationAuditContext.getCategory())
.action(operationAuditContext.getAction())
.description(operationAuditContext.getDescription())
.startAt(operationAuditContext.getStartAt())
.endAt(operationAuditContext.getEndAt())
.success(operationAuditContext.getSuccess())
.errorMsg(operationAuditContext.getErrorMsg())
.build();
Optional.ofNullable(operationAuditContext.getBeforeValues()).ifPresent(map -> cmd.setBeforeValues(JSON.toJSONString(map)));
Optional.ofNullable(operationAuditContext.getAfterValues()).ifPresent(map -> cmd.setAfterValues(JSON.toJSONString(map)));
if (operationAuditContext.getSuccess()) {
var res = (TokenVO) operationAuditContext.getResponse();
var token = res.getToken();
Optional.ofNullable(jwtFacade.parse(token)).ifPresent(map -> {
Optional.ofNullable(map.get(ACTOR_USER_ID)).ifPresent(userId -> cmd.setActorId(String.valueOf(userId)));
Optional.ofNullable(map.get(ACTOR_TENANT_ID)).ifPresent(tenantId -> cmd.setTenantId(String.valueOf(tenantId)));
Optional.ofNullable(map.get(ACTOR_ROLE)).ifPresent(actorRole -> cmd.setActorRole(String.valueOf(actorRole)));
});
}
operationLogProvider.createOperationLog(cmd); //写入数据库
}
}
在注解中指定handler实现类
@OperationAudit(type = "LOGIN", category = "ORG", action = "LOGIN", description = "用户登录", handler = LoginOperationHandler.class)