背景
在现代应用系统中,事件审计是一个至关重要的功能。通过记录用户的操作行为,我们可以追踪问题、分析用户行为,甚至在出现安全问题时提供关键证据。由于目前没有较好的事件审计框架,笔者决定实现一套可扩展的事件审计组件,要求对业务低侵入性,可以轻松获取前后变更的内容。
目标
提供自定义注解给业务侧,实现开箱即用的事件审计存储功能。
实现
审计
我们定义了 @EventAuditor 注解,关键字段包括 operator(操作人)、content(操作内容模板)、bizScenario(事件发生的场景)等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface EventAuditor {
String operator() default Strings.EMPTY;
String role() default Strings.EMPTY;
String bizScenario() default Strings.EMPTY;
String content() default Strings.EMPTY;
String extra() default Strings.EMPTY;
String condition() default Strings.EMPTY;
boolean evalBeforeInvoke() default true;
boolean recordReturnValue() default false; }
|
通过 Spring 的 MethodInterceptor,我们实现了 EventAuditorInterceptor 来捕获 @EventAuditor 注解,并在方法调用前后进行审计记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
| @RequiredArgsConstructor @Slf4j public class EventAuditorInterceptor implements MethodInterceptor {
private static final String RETURN = "_return";
private static final String ERROR_MSG = "_errorMsg";
private final EventAuditorConfig eventAuditorConfig;
private AsyncTaskExecutor asyncTaskExecutor;
@Override public Object invoke(@NotNull MethodInvocation invocation) throws Throwable { if (AopUtils.isAopProxy(invocation.getThis())) { return invocation.proceed(); }
LocalDateTime now = LocalDateTime.now(); Method method = invocation.getMethod(); EventAuditor[] eventAuditors; try { eventAuditors = method.getAnnotationsByType(EventAuditor.class); } catch (Throwable throwable) { return invocation.proceed(); }
List<AuditingEvent> events = Lists.newArrayList(); events.addAll(parseList(invocation, eventAuditors, true));
Object result = null; boolean success = true; long executionCost = 0L; String errorMsg = null; String watchId = invocation.getClass() + Strings.DOT + invocation.getMethod().getName(); StopWatch stopWatch = new StopWatch(watchId); stopWatch.start(); try { result = invocation.proceed(); } catch (Throwable e) { if (stopWatch.isRunning()) { stopWatch.stop(); executionCost = stopWatch.getTotalTimeMillis(); } success = false; errorMsg = e.getMessage(); SpelEvaluationContext.setVariable(ERROR_MSG, errorMsg); throw e; } finally { if (stopWatch.isRunning()) { stopWatch.stop(); executionCost = stopWatch.getTotalTimeMillis(); } SpelEvaluationContext.setVariable(RETURN, result); events.addAll(parseList(invocation, eventAuditors, false)); for (AuditingEvent event : events) { event.setSuccess(success); event.setOperateDate(now); event.setExecutionCost(executionCost); if (errorMsg != null) { event.setThrowable(errorMsg); } }
send(events); SpelEvaluationContext.remove(); } return result; }
public void setAsyncTaskExecutor(AsyncTaskExecutor asyncTaskExecutor) { this.asyncTaskExecutor = asyncTaskExecutor; }
private void send(List<AuditingEvent> events) { String senderType = eventAuditorConfig.getSender().getSenderType(); EventSenderBuilder eventSenderBuilder = ExtensionLoader.getExtensionLoader(EventSenderBuilder.class).getExtension(senderType); eventSenderBuilder.setEventAuditorConfig(eventAuditorConfig); EventSender eventSender = eventSenderBuilder.build(); if (eventAuditorConfig.getSender().isAsync() && asyncTaskExecutor != null) { asyncTaskExecutor.execute(() -> eventSender.send(events)); } else { eventSender.send(events); } }
private List<AuditingEvent> parseList(@NotNull MethodInvocation invocation, EventAuditor[] eventAuditors, boolean evalBeforeInvoke) { List<AuditingEvent> events = Lists.newArrayList(); for (EventAuditor eventAuditor : eventAuditors) { if (eventAuditor.evalBeforeInvoke() == evalBeforeInvoke) { AuditingEvent auditingEvent = this.parseModel(eventAuditor, invocation); if (auditingEvent != null) { events.add(auditingEvent); } } } return events; }
private AuditingEvent parseModel(EventAuditor eventAuditor, MethodInvocation invocation) { EvaluationContext context = SpelEvaluationContext.getContext(); CustomFunctionRegistrar.register((StandardEvaluationContext) context); Method method = invocation.getMethod(); String[] parameterNames = SpelExpressionEvaluator.getParameterNameDiscoverer().getParameterNames(method); Object[] arguments = invocation.getArguments(); if (parameterNames != null) { int len = parameterNames.length; for (int i = 0; i < len; i++) { context.setVariable(parameterNames[i], arguments[i]); } }
ExpressionParser parser = SpelExpressionEvaluator.getExpressionParser(); if (StringUtils.isNotBlank(eventAuditor.condition())) { Expression expression = parser.parseExpression(eventAuditor.condition()); if (!Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) { return null; } }
AuditingEvent auditingEvent = new AuditingEvent(); if (StringUtils.isNotBlank(eventAuditor.operator())) { Expression expression = parser.parseExpression(eventAuditor.operator()); auditingEvent.setOperator(expression.getValue(context, String.class)); }
if (StringUtils.isNotBlank(eventAuditor.role())) { Expression expression = parser.parseExpression(eventAuditor.role()); auditingEvent.setRole(expression.getValue(context, String.class)); }
if (StringUtils.isNotBlank(eventAuditor.bizScenario())) { Expression expression = parser.parseExpression(eventAuditor.bizScenario()); auditingEvent.setBizScenario(expression.getValue(context, String.class)); }
if (StringUtils.isNotBlank(eventAuditor.content())) { Expression expression = parser.parseExpression(eventAuditor.content()); auditingEvent.setContent(expression.getValue(context, String.class)); }
if (StringUtils.isNotBlank(eventAuditor.extra())) { Expression expression = parser.parseExpression(eventAuditor.extra()); Object extra = expression.getValue(context, Object.class); auditingEvent.setExtra(extra instanceof String ? (String) extra : JSONHelper.json().toJSONString(extra)); } return auditingEvent; } }
|
上述代码中有 3 个关键类,分别是:
SpelExpressionEvaluator :处理 SpEL 表达式。
CustomFunctionRegistrar:存储自定义函数和 Java 方法的关联关系,解析自定义函数时,通过缓存找到对应的 Java 方法反射调用。
EventSenderBuilder :将事件内容往其他渠道发送,例如消息队列、数据库,便于存储。
接下来依次展开说明。
SpEL 表达式解析实现
SpEL 是 Spring 特有的表示解析格式,内部通过 StandardEvaluationContext 实现,我们可以在这个基础上进行封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public class SpelExpressionEvaluator {
private static final ExpressionParser PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
public static ExpressionParser getExpressionParser() { return PARSER; }
public static ParameterNameDiscoverer getParameterNameDiscoverer() { return DISCOVERER; }
public static String parseExpression(String expressionString, Method method, Object[] arguments) { String[] params = DISCOVERER.getParameterNames(method); if (params != null && params.length > 0) { for (int i = 0; i < params.length; i++) { SpelEvaluationContext.setVariable(params[i], arguments[i]); } }
Expression expression = PARSER.parseExpression(expressionString); return expression.getValue(SpelEvaluationContext.getContext(), String.class); } }
public class SpelEvaluationContext {
private static final TransmittableThreadLocal<EvaluationContext> VARIABLES = new TransmittableThreadLocal<EvaluationContext>() {
@Override protected EvaluationContext initialValue() { return new StandardEvaluationContext(); } };
public static EvaluationContext getContext() { return VARIABLES.get(); }
public static Object lookupVariable(String key) { EvaluationContext context = getContext(); return context.lookupVariable(key); }
public static void setVariable(String key, Object value) { EvaluationContext context = getContext(); context.setVariable(key, value); VARIABLES.set(context); }
public static void remove() { VARIABLES.remove(); } }
|
自定义函数解决上下文历史
通过 MethodInterceptor 方法拦截只能获取入参和返回值,对于修改内容的场景,没办法获取原始记录。我们引入了 CustomFunctionRegistrar 自定义函数,由用户来决定调用哪个 Java 方法。
在应用启动时,基于 Spring 自动扫描所有标记了 CustomFunctionRegistrar 的代码,通过 AOP 生成代理,将自定义函数和代理方法的映射存到缓存中,这样就可以通过自定义函数找到代理方法,调用业务指定的逻辑了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
| @Slf4j public class CustomFunctionRegistrar implements ApplicationContextAware {
private static final Map<String, Method> FUNCTION_CACHE = new ConcurrentHashMap<>();
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { initialize(applicationContext); }
private void initialize(ApplicationContext applicationContext) { Map<String, Object> components = applicationContext.getBeansWithAnnotation(Component.class); components.values().forEach(component -> { Method[] methods = component.getClass().getMethods(); if (methods.length == 0) { return; }
Object targetObject = AopUtils.getDynamicProxyTargetObject(component); for (Method method : methods) { CustomFunction annotation = AnnotatedElementUtils.findMergedAnnotation(method, CustomFunction.class); if (annotation == null) { continue; } if (ReflectionUtils.isStaticMethod(method)) { cache(annotation, method); continue; } ClassPool pool = ClassPool.getDefault(); Class<?> targetClass = targetObject.getClass(); String staticallyClassName = targetClass.getName() + "_Statically"; Class<?> delegateClass; CtClass ctClass = pool.getOrNull(staticallyClassName); try { if (ctClass == null) { ctClass = constructCtClass(method, pool, targetClass, staticallyClassName); delegateClass = ctClass.toClass(); } else { delegateClass = ctClass.getClass().getClassLoader().loadClass(staticallyClassName); } Object proxy = delegateClass.getConstructor(targetClass).newInstance(component); Method[] proxyMethods = proxy.getClass().getDeclaredMethods(); Arrays.stream(proxyMethods).forEach(proxyMethod -> { if (Arrays.equals(method.getParameterTypes(), proxyMethod.getParameterTypes()) && method.getName().equals(proxyMethod.getName())) { cache(annotation, proxyMethod); } }); } catch (Exception e) { throw new RuntimeException(e); } } }); }
public static void register(StandardEvaluationContext context) { FUNCTION_CACHE.forEach(context::registerFunction); }
private static void cache(CustomFunction annotation, Method method) { String registerName = StringUtils.hasText(annotation.value()) ? annotation.value() : method.getName(); FUNCTION_CACHE.put(registerName, method); log.info("Register custom function '{}' as name '{}'", method, registerName); }
private CtClass constructCtClass(Method method, ClassPool pool, Class<?> targetClass, String staticallyClassName) throws NotFoundException, CannotCompileException { CtClass ctClass = pool.makeClass(staticallyClassName); ctClass.addInterface(pool.get(Serializable.class.getName()));
CtField field = new CtField(pool.get(targetClass.getName()), "delegating", ctClass); field.setModifiers(javassist.Modifier.STATIC | javassist.Modifier.PROTECTED); ctClass.addField(field);
CtConstructor constructor = new CtConstructor(new CtClass[]{pool.get(targetClass.getName())}, ctClass); constructor.setBody("{delegating = $1;}"); ctClass.addConstructor(constructor);
CtMethod getterMethod = new CtMethod(pool.get(targetClass.getName()), "getDelegating", new CtClass[]{}, ctClass); getterMethod.setModifiers(javassist.Modifier.PUBLIC); getterMethod.setBody("{return delegating;}"); ctClass.addMethod(getterMethod);
int modifier = method.getModifiers(); modifier |= javassist.Modifier.STATIC; String methodName = method.getName(); Class<?> returnType = method.getReturnType(); StringBuilder builder = new StringBuilder(); builder.append(chooseModifier(modifier)).append(" ") .append(returnType.getName()).append(" ") .append(methodName).append("(");
Class<?>[] parameterType = method.getParameterTypes(); StringBuilder params = null; for (int i = 0; i < parameterType.length; i++) { builder.append(parameterType[i].getName()).append(" "); builder.append("$_").append(i).append(","); if (params == null) { params = new StringBuilder(); } params.append("$_").append(i).append(","); } if (params != null) { builder.delete(builder.length() - 1, builder.length()); params.delete(params.length() - 1, params.length()); } builder.append(")"); builder.append("{"); if (!returnType.equals(void.class)) { builder.append("return").append(" "); } builder.append("delegating.").append(methodName).append("("); if (params != null) { builder.append(params); } builder.append(")").append(";"); builder.append("}");
CtMethod ctMethod = CtMethod.make(builder.toString(), ctClass); ctClass.addMethod(ctMethod); return ctClass; }
private String chooseModifier(int modifier) { StringBuilder builder = new StringBuilder(); if ((modifier & javassist.Modifier.PUBLIC) == javassist.Modifier.PUBLIC) { builder.append("public").append(" "); } if ((modifier & javassist.Modifier.PRIVATE) == javassist.Modifier.PRIVATE) { builder.append("private").append(" "); } if ((modifier & javassist.Modifier.PROTECTED) == javassist.Modifier.PROTECTED) { builder.append("protected").append(" "); } if ((modifier & javassist.Modifier.ABSTRACT) == javassist.Modifier.ABSTRACT) { builder.append("abstract").append(" "); } if ((modifier & javassist.Modifier.STATIC) == javassist.Modifier.STATIC) { builder.append("static").append(" "); } if ((modifier & javassist.Modifier.FINAL) == javassist.Modifier.FINAL) { builder.append("final").append(" "); } return builder.toString(); } }
|
审计事件内容存储实现
拦截到审计信息后,接下来要做的事情就是存储。我们定义了 EventSender 接口和 AuditingEvent 审计事件模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| public interface EventSender {
void send(List<AuditingEvent> events); }
@Accessors(chain = true) @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode @ToString @Data public class AuditingEvent {
private String operator;
private String role;
private LocalDateTime operateDate;
private String bizScenario;
private String content;
private String extra;
private String returnValue;
private Long executionCost;
private Boolean success;
private String throwable; }
|
在前面的 EventAuditorInterceptor#send() 方法,就是调用了这个接口。
关于接口的实现,我们预留了扩展点,可以本地日志打印、可以发送到 MQ 或者数据库,例如,本地日志打印实现代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @RequiredArgsConstructor @Slf4j public class LoggingEventSender implements EventSender {
public static final String AUDITING_EVENT = "Auditing event: {}";
private final Level level;
@Override public void send(List<AuditingEvent> events) { if (CollectionUtils.isEmpty(events)) { return; }
List<String> contents = events.stream() .map(AuditingEvent::getContent).collect(Collectors.toList()); switch (level) { case DEBUG: log.debug(AUDITING_EVENT, contents); break; case INFO: log.info(AUDITING_EVENT, contents); break; case WARN: log.warn(AUDITING_EVENT, contents); break; } } }
|
为了简化配置,我们基于 Spring Boot 的自动装配机制进一步封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| @ConditionalOnProperty( prefix = EventAuditorProperties.PREFIX, name = Conditions.ENABLED, havingValue = Conditions.TRUE, matchIfMissing = true ) @AutoConfigureAfter(AsyncTaskExecutionAutoConfiguration.class) @EnableEventAuditor @EnableConfigurationProperties(EventAuditorProperties.class) @Slf4j @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Configuration(proxyBeanMethods = false) public class EventAuditorAutoConfiguration {
}
@Setter @Getter @ConfigurationProperties(prefix = EventAuditorProperties.PREFIX) public class EventAuditorProperties extends EventAuditorConfig {
public static final String PREFIX = "event-auditor";
private boolean enabled; }
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Slf4j @Configuration(proxyBeanMethods = false) public class EventAuditorConfiguration implements ImportAware {
private AnnotationAttributes enableEventAuditor;
@Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.enableEventAuditor = AnnotationAttributes.fromMap( importMetadata.getAnnotationAttributes(EnableEventAuditor.class.getName(), false)); if (this.enableEventAuditor == null) { log.warn("@EnableEventAuditor is not present on importing class"); } }
@Bean public EventAuditorInterceptor eventAuditorInterceptor(ObjectProvider<EventAuditorConfig> eventAuditorConfig, ObjectProvider<AsyncTaskExecutor> asyncTaskExecutor) { EventAuditorInterceptor interceptor = new EventAuditorInterceptor(eventAuditorConfig.getIfUnique(EventAuditorConfig::new)); asyncTaskExecutor.ifAvailable(interceptor::setAsyncTaskExecutor); return interceptor; }
@Bean public EventAuditorPointcutAdvisor eventAuditorPointcutAdvisor(EventAuditorInterceptor eventAuditorInterceptor) { EventAuditorPointcutAdvisor pointcutAdvisor = new EventAuditorPointcutAdvisor(); pointcutAdvisor.setAdviceBeanName("eventAuditorPointcutAdvisor"); pointcutAdvisor.setAdvice(eventAuditorInterceptor); if (enableEventAuditor != null) { pointcutAdvisor.setOrder(enableEventAuditor.getNumber("order")); } return pointcutAdvisor; }
@Bean public CustomFunctionRegistrar customFunctionRegistrar() { return new CustomFunctionRegistrar(); } }
@EqualsAndHashCode @ToString @Setter @Getter public class EventAuditorConfig {
private final Sender sender = new Sender();
@EqualsAndHashCode @ToString @Setter @Getter public static class Sender {
private String senderType = "logging";
private boolean async = true;
private final Logging logging = new Logging();
private final Kafka kafka = new Kafka();
private final RocketMQ rocketMQ = new RocketMQ();
@EqualsAndHashCode @ToString @Setter @Getter public static class Logging {
private Level level = Level.INFO; }
@EqualsAndHashCode @ToString @Setter @Getter public static class Kafka {
private String topic; }
@EqualsAndHashCode @ToString @Setter @Getter public static class RocketMQ {
private String topic;
private String namespace;
private String tags;
private String keys; } } }
|
从配置类可以看到,我们为消息存储提供了 3 个选项: Logging 本地日志、RocketMQ 或 Kafka。
代码使用示例
假设我们在 UserService#modifyUser() 修改用户信息,需要记录修改了哪些内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Service("userService") public class UserServiceImpl implements UserService {
@EventAuditor(bizScenario = "'demo.users.getUserById'", operator = "#operator", content = "'用户' + #cmd.login + '修改了邮箱,从' + #queryOldEmail(#cmd.id) + '修改为' + #cmd.email") @Transactional(rollbackFor = Exception.class) @Override public Response modifyUser(UserModifyCmd cmd) { return userModifyCmdExe.execute(cmd); }
@CustomFunction("queryOldEmail") public String queryOldEmail(Long id) { return this.getUserById(UserByIdQry.builder().id(id).build()).getData().getEmail(); } }
|
对应的 application.yaml 配置文件设置如下。
1 2 3 4 5 6 7 8 9 10 11
| event-auditor: enabled: true sender: type: logging
|
因为笔者配置了 event-auditor.sender.type=logging,所以,审计内容输出到控制台日志,如下图,当调用用户信息更新接口,在控制台打印了 用户admin修改了邮箱 字眼。

产出
对业务代码的侵入性极小,扩展性很高,可以轻松实现事件审计的记录,并存储到你想要的位置。
本文涉及的代码完全开源,感兴趣的伙伴可以查阅 eden-event-auditor 和 eden-event-auditor-spring-boot-starter。