解决若依框架中 AI 工具回调的权限上下文丢失问题

摘要

在使用若依(RuoYi)这类基于 Spring MVC 架构的应用中集成 Spring AI 进行工具(Tool Call)调用时,我们经常会遇到 java.lang.IllegalStateException: No thread-bound request found 权限报错。本文将深入分析该问题,并提供一套基于 MVC/WebFlux 双模上下文持有者的通用解决方案,确保在异步的 AI 工具回调线程中也能正确获取并应用用户的数据权限(Data Scope)。

一、问题根源:ThreadLocal 上下文的局限性

若依框架广泛采用 Spring MVC 的 ThreadLocal 机制来存储会话级信息,例如:

  • 权限信息:使用 SecurityUtils.getLoginUser() 获取当前登录用户及其权限集。
  • 请求上下文:使用 RequestContextHolder 存储当前 HTTP 请求的属性(例如,在 PermissionContextHolder.isMvcRequest() 中尝试访问)。

然而,当 Spring AI 引擎进行工具调用(Tool Call)时,它通常会在一个独立的、异步的线程中执行该工具方法(例如,一个 ExecutorService 线程或 WebFlux 的 Reactor 线程)。

核心矛盾点在于
异步线程无法继承原始 HTTP 请求线程的 ThreadLocal 变量。当工具方法内部的权限切面(如 DataScopeAspect)尝试通过 RequestContextHolder 获取上下文时,由于 ThreadLocal 为空,便会抛出 No thread-bound request found 异常。

二、解决方案:双模上下文持有者(Dual-Mode Context Holder)

解决之道在于抽象出权限上下文的存储和获取机制,使其能够同时支持同步的 ThreadLocal (MVC) 和异步的 Reactive Context (WebFlux/AI) 两种模式。

我们引入了 PermissionContextHolder,并利用它来封装线程检测和上下文传递逻辑。

1. 上下文持有者 PermissionContextHolder

PermissionContextHolder 的核心功能是判断当前线程环境并提供对应的上下文存取方法。

关键代码逻辑

  • 环境检测isMvcRequest() 通过尝试访问 RequestContextHolder 来判断是否处于 MVC 线程。
  • WebFlux/AI 异步上下文:使用 Project Reactor 的 Mono.deferContextualcontextWrite 来进行上下文的存储和获取,以适应异步调用链。
public class PermissionContextHolder {
    // ...
    // 判断当前线程是否是 MVC 请求
    public static boolean isMvcRequest() {
        try {
            RequestContextHolder.currentRequestAttributes();
            return true;
        } catch (IllegalStateException e) {
            // MVC ThreadLocal 不可用,判断为异步环境
            return false;
        }
    }

    // WebFlux/AI 异步环境获取上下文
    public static Mono<String> getReactiveContext() {
        return Mono.deferContextual(ctx -> Mono.justOrEmpty(ctx.get(PERMISSION_CONTEXT_ATTRIBUTES)));
    }

    // WebFlux/AI 异步环境设置上下文
    public static <T> reactor.core.publisher.Mono<T> withReactiveContext(String permission, reactor.core.publisher.Mono<T> mono) {
        return mono.contextWrite(ctx -> ctx.put(PERMISSION_CONTEXT_ATTRIBUTES, permission));
    }
}

2. 权限服务中的上下文注入 PermissionService

在若依的权限校验服务 PermissionService 中,我们利用 isMvcRequest() 在权限校验时提前将权限字符串(如 system:user:list)注入到正确的上下文中。

关键代码逻辑

  • MVC 模式:权限直接存入 ThreadLocal (PermissionContextHolder.setMvcContext(permission)).
  • AI/异步模式:权限通过 PermissionContextHolder.withReactiveContext(...) 写入 Mono 的上下文,并使用 .block() 阻塞等待,以适配 Spring AI 往往需要同步返回结果的工具方法调用。
@Service("ss")
public class PermissionService {
    // ...

    public boolean hasPermi(String permission) {
        // ... [权限检查]

        if (isMvcRequest()) {
            // MVC 情况:设置 ThreadLocal
            PermissionContextHolder.setMvcContext(permission);
            return hasPermissions(loginUser.getPermissions(), permission);
        }

        // WebFlux/AI Tooling 情况:将权限写入 Reactive Context
        return Boolean.TRUE.equals(PermissionContextHolder.withReactiveContext(permission,
                Mono.just(hasPermissions(loginUser.getPermissions(), permission))
        ).block()); 
        // 注意:这里的 .block() 是关键,它允许同步工具调用方法获取异步结果。
    }
}

3. 数据权限切面中的上下文获取 DataScopeAspect

最后,在执行数据过滤逻辑的切面中,我们也必须使用双模逻辑来获取权限信息。

关键代码逻辑

  • MVC 模式:直接从 PermissionContextHolder.getMvcContext() 获取 String 权限。
  • AI/异步模式:从 PermissionContextHolder.getReactiveContext() 获取 Mono<String>,然后通过 .subscribe() 订阅获取权限值,并执行数据过滤 (dataScopeFilter)。
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
    // ... [用户检查]

    if (PermissionContextHolder.isMvcRequest()) {
        // MVC 线程
        String permission = PermissionContextHolder.getMvcContext();
        dataScopeFilter(joinPoint, currentUser, ..., permission);
    } else {
        // WebFlux/AI 异步线程
        PermissionContextHolder.getReactiveContext().defaultIfEmpty(controllerDataScope.permission())
                .subscribe(permission -> dataScopeFilter(joinPoint, currentUser,
                        controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission));
    }
}

三、总结与实践意义

通过这套双模上下文机制,我们成功地在若依框架中解决了 Spring AI 异步工具调用中权限上下文丢失的问题:

  • 解决了 IllegalStateException:异步线程不再依赖不可用的 ThreadLocal
  • 确保了数据安全:无论权限校验发生在 Web 请求线程还是 AI Tooling 异步线程,都能正确获取到当前请求关联的权限信息,并执行数据过滤。

这套模式对于任何在传统 Spring MVC 项目中引入异步/响应式技术栈(如 Spring AI、WebClient、CompletableFuture 等)的场景都具有极高的借鉴价值。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐