文章
问答
冒泡
spring 项目中如何进行行为审计

在业务系统中,我们经常会需要记录用户的操作记录,从而方便对用户的行为进行审计。得益于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)

github地址:https://github.com/trionesdev/triones-spring-commons/tree/develop/triones-spring-core/src/main/java/com/trionesdev/spring/core/audit

spring boot

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读