多租户系统中异常审计必须保障租户上下文全程可靠:入口处filter/interceptor解析x-tenant-id并绑定threadlocal;异步任务需显式传递;异常处理器判空兜底、注入mdc、响应体透传;结合opentelemetry跨进程传播tenant.id;日志强制结构化输出tenant_id字段。

在多租户系统中,全局异常捕获器(如 Spring 的 @ControllerAdvice 或 Logback 的 UncaughtExceptionHandler)本身不携带请求上下文,若直接在其中读取 ThreadLocal 中的租户 ID,极易因线程复用、异步调用或上下文未注入而返回空值或错误值——这会导致审计日志缺失租户标识,无法追溯异常归属,严重削弱安全合规能力。
确保租户上下文在异常发生前已可靠注入
租户 ID 必须在请求入口(如 Filter、Interceptor 或 WebMvcConfigurer 注册的 HandlerInterceptor)完成解析与绑定,并严格保障其生命周期覆盖整个请求链路:
- 从
X-Tenant-ID请求头提取后,立即调用TenantContext.set(tenantId),且该操作必须在业务逻辑执行前完成 - 避免在 Controller 层才首次设置上下文,否则中间件、AOP 或数据库拦截器可能已执行,导致部分操作无租户标记
- 对异步任务(如
@Async、CompletableFuture、虚拟线程)需显式传递并重置上下文,InheritableThreadLocal仅对普通子线程有效,对 ForkJoinPool 或线程池中的线程无效
在异常处理器中安全读取并透传租户上下文
全局异常处理器应将租户 ID 视为“可选但关键”字段,不可强依赖其存在,而需配合兜底策略保证审计完整性:
- 使用
TenantContext.getCurrentTenant()获取值,但始终判空;若为空,记录 warn 级日志并标注 “tenant_context_missing”,而非静默跳过 - 将租户 ID 注入 MDC(Mapped Diagnostic Context),使后续所有日志(包括异常堆栈)自动携带该字段:
MDC.put("tenant_id", Optional.ofNullable(TenantContext.getCurrentTenant()).orElse("unknown")); - 在异常响应体中显式返回租户标识(如 JSON 响应含
"tenant_id": "t-abc123"),便于网关或前端做二次校验与归因
结合 OpenTelemetry 实现带租户标签的全链路异常追踪
单纯依赖线程局部变量不足以支撑分布式场景下的审计溯源。需将租户上下文升级为跨进程传播的 trace 属性:
- 在请求入口处,将
X-Tenant-ID写入 OpenTelemetry 的SpanAttributes:span.setAttribute("tenant.id", tenantId); - 配置 OTel SDK 启用 context propagation(如 B3、W3C TraceContext),确保下游服务、消息队列消费者、定时任务等都能继承该属性
- 在全局异常处理器中,通过
OpenTelemetry.getGlobalTracer().getCurrentSpan()获取当前 span,并从中提取tenant.id,写入审计事件或告警系统
审计日志格式必须强制包含租户上下文字段
无论异常是否由本租户触发,每条审计日志都应结构化输出租户标识,禁止使用模糊占位符(如 “N/A”、“-”、“*”):
- 推荐日志模板:
%d{ISO8601} [%X{tenant_id:-unknown}] [%X{trace_id:-none}] [%X{span_id:-none}] [%p] %c - %m%n - 对敏感操作(如删除知识库、修改模型配置)的日志,额外增加
op_tenant_id字段,与请求头中的X-Tenant-ID做一致性比对,发现不匹配立即告警 - 日志落盘前,由统一审计过滤器检查字段完整性;若
tenant_id缺失且非系统级内部调用(如健康检查),则拒绝写入并触发监控告警











