若依框架和阿里AgentScope的权限封装
2026/7/29 11:08:10 网站建设 项目流程

若依框架和阿里AgentScope的权限封装

一、背景

在若依体系中,后台接口通常使用 Spring Security 的@PreAuthorize进行权限控制:

@PreAuthorize("@ss.hasPermission('water-meter:order:query')")

在接入阿里 AgentScope Java 后,我们还可以使用@Tool将 Java 方法注册成大模型工具:

@Tool(name="get_water_order",description="按订单编号查询一笔水费订单",readOnly=true)publicStringgetWaterOrder(LongorderId){// 查询订单}

但仅仅把@PreAuthorize@Tool放在同一个方法上,并不能完整解决 Agent 工具权限问题。

@PreAuthorize发生在工具真正执行时。在此之前,工具名称、描述和参数 JSON Schema 可能已经发送给大模型。无权限用户虽然无法成功执行工具,但仍然可能在模型上下文中看到工具能力。

本文要实现的目标是:

当前对话用户没有某个权限时,对应 Tool Schema 不得进入本轮模型上下文,而不只是等到工具执行时再拒绝。

同时还需要满足以下约束:

  1. HarnessAgent是应用级单例。
  2. Toolkit在启动时一次性构建,不能按用户重复创建。
  3. 同一个(userId, sessionId)的请求由 AgentScope 串行处理,不同会话可以并行。
  4. 不同用户并发访问时,工具权限不能相互污染。
  5. 工具执行阶段仍然保留@PreAuthorize作为第二道防线。

二、为什么不能只使用@PreAuthorize

一次典型的 Agent 工具调用分为两个阶段。

1. 模型推理阶段

应用把 Tool Schema 发送给模型:

{"name":"get_water_order","description":"按订单编号查询一笔水费订单","parameters":{"type":"object","properties":{"orderId":{"type":"integer"}}}}

模型根据用户问题以及可见的 Tool Schema,决定是否生成工具调用。

2. 工具执行阶段

模型生成工具调用后,AgentScope 找到对应的AgentTool并执行 Java 方法。Spring Method Security 才会在这个阶段处理@PreAuthorize

因此:

Tool Schema 发送给模型 ↓ 模型选择工具 ↓ 执行 Java 方法 ↓ @PreAuthorize 校验

如果只依赖@PreAuthorize,权限校验发生得太晚。

三、为什么不使用@Aspect + @Around

很多开发者看到“注解权限”后,第一反应是编写 Spring AOP:

@Pointcut("@annotation(AgentToolPermission)")publicvoidagentToolPermissionPointcut(){}@Around("agentToolPermissionPointcut()")publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{// 权限判断returnjoinPoint.proceed();}

这种实现仍然只能拦截 Java 方法执行。等环绕通知被触发时,Tool Schema 已经发送给模型,因此它只能实现“无权限时不允许执行”,无法实现“无权限时模型根本看不到工具”。

如果把切点放在 Controller:

@Around("execution(* com.example.agent.controller..*(..))")

虽然能够拦截对话接口,但此时 AgentScope 还没有组装ReasoningInput.tools,切面也拿不到本轮即将发送给模型的 Tool Schema。

AgentScope 官方提供的 Middleware 才是正确扩展点。onReasoning位于每轮 ReAct 推理过程,并且能够读取和替换:

ReasoningInput(List<Msg>messages,List<ToolSchema>tools,GenerateOptionsoptions)

因此可以在模型调用前重新构造ReasoningInput,只保留当前用户允许看到的工具。

四、总体设计

完整实现分为启动阶段和请求阶段。

1. 启动阶段

系统启动时:

  1. 单例Toolkit注册全部业务工具。
  2. 扫描工具方法上的@AgentToolPermission
  3. 建立“权限标识 → 工具名称”的不可变索引。
  4. 创建单例HarnessAgent和单例权限 Middleware。

例如:

water-meter:order:query ├── get_water_order └── get_latest_water_order_by_meter_code water-meter:order:settle-abnormal └── settle_abnormal_water_order

这里建立的不是用户权限,而是权限标识与 Tool Schema 的静态映射。

2. 请求阶段

每次后台用户发起对话时:

  1. 在 HTTP 请求线程调用若依权限服务。
  2. 生成当前用户不可变的工具权限快照。
  3. 将快照放入本次调用独有的RuntimeContext
  4. onReasoningRuntimeContext读取快照。
  5. 过滤ReasoningInput.tools
  6. 将过滤后的 Tool Schema 发送给模型。
  7. 工具真正执行时,再由@PreAuthorize复核权限。

整体流程如下:

后台登录用户 ↓ SecurityFrameworkService.hasPermission(...) ↓ AgentToolAccessContext 权限快照 ↓ RuntimeContext(per-call) ↓ AgentToolPermissionMiddleware.onReasoning(...) ↓ 过滤 ReasoningInput.tools ↓ 模型只看到有权限的 Tool Schema ↓ 工具执行时再次经过 @PreAuthorize

五、RuntimeContext 不是模型上下文

RuntimeContext中的 “Context” 很容易被误认为大模型上下文窗口。

模型实际获得的输入主要是:

ReasoningInput ├── messages ├── tools └── options

RuntimeContext是 AgentScope 的服务端运行上下文:

RuntimeContext ├── userId ├── sessionId ├── AgentState 引用 └── extra 请求级数据

写入RuntimeContext.extra的 Java 对象不会自动进入messages、system prompt 或 Tool Schema,也不会自动发送给模型。

权限信息放入RuntimeContext有三个原因:

  1. HarnessAgent和 Middleware 是单例,不能把当前用户权限写进实例成员变量。
  2. 模型调用和工具执行可能切换到 Reactor 工作线程,不能一直依赖原 HTTP 线程的SecurityContextHolder
  3. RuntimeContext是每次callstreamEvents独立的,适合在本次调用的 Middleware 和 Tool 之间传递服务端数据。

六、声明工具权限注解

定义一个只负责保存权限标识的注解:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceAgentToolPermission{/** * 与 @PreAuthorize 中使用的若依权限标识保持一致。 */Stringvalue();}

该注解不是 AOP 通知,不会主动执行任何逻辑。它只是声明式元数据,由权限解析器在启动时通过反射读取。

七、定义请求级权限快照

publicrecordAgentToolAccessContext(List<String>activatedGroups,Authenticationauthentication,LongtenantId){publicAgentToolAccessContext{activatedGroups=activatedGroups==null?List.of():List.copyOf(activatedGroups);}}

需要注意:

  1. 集合使用List.copyOf创建不可变副本。
  2. authentication只用于工具执行线程临时恢复 Spring Security 上下文。
  3. 不保存 Bearer Token 和密码。
  4. 该对象不会主动序列化进 AgentState。

八、启动时建立权限和工具的映射

权限解析器扫描 Spring 工具 Bean 的目标类:

publicStringresolveRequiredPermission(ObjecttoolCandidate){Class<?>targetClass=AopUtils.getTargetClass(toolCandidate);Set<String>permissions=newLinkedHashSet<>();for(Methodmethod:targetClass.getMethods()){AgentToolPermissionpermission=method.getAnnotation(AgentToolPermission.class);if(permission!=null){if(StrUtils.isBlank(permission.value())){thrownewIllegalStateException("@AgentToolPermission 权限标识不能为空");}permissions.add(permission.value());}}if(permissions.size()>1){thrownewIllegalStateException("同一个工具类混合了多个权限域,请按权限拆分工具类");}returnpermissions.stream().findFirst().orElse(null);}

建议同一个工具类只属于一个权限域。例如:

WaterMeterOrderQueryTools → water-meter:order:query WaterMeterOrderSettlementTools → water-meter:order:settle-abnormal

查询和写操作拆分后,可以避免只拥有查询权限的用户看到结算工具。

九、每次请求计算当前用户权限

publicAgentToolAccessContextresolveCurrentUserAccessContext(){List<String>activatedGroups=permissionGroupMappings.entrySet().stream().filter(entry->securityFrameworkService.hasPermission(entry.getKey())).map(Map.Entry::getValue).toList();Authenticationsource=SecurityContextHolder.getContext().getAuthentication();Authenticationsnapshot=source==null?null:newUsernamePasswordAuthenticationToken(source.getPrincipal(),null,source.getAuthorities());returnnewAgentToolAccessContext(activatedGroups,snapshot,TenantContextHolder.getTenantId());}

这里复用了若依框架的:

SecurityFrameworkService.hasPermission(permission)

因此其权限语义与:

@PreAuthorize("@ss.hasPermission('xxx')")

保持一致。

用户权限不是在应用启动时缓存的,而是在每次对话请求进入时重新计算。

十、通过 Middleware 过滤 Tool Schema

@Slf4jpublicclassAgentToolPermissionMiddlewareimplementsMiddlewareBase{privatefinalAgentToolPermissionResolverpermissionResolver;@AutowiredpublicAgentToolPermissionMiddleware(AgentToolPermissionResolverpermissionResolver){this.permissionResolver=permissionResolver;}@OverridepublicFlux<AgentEvent>onReasoning(Agentagent,RuntimeContextcontext,ReasoningInputinput,Function<ReasoningInput,Flux<AgentEvent>>next){AgentToolAccessContextaccessContext=context.get(AgentToolAccessContext.class);List<String>activatedGroups=accessContext==null?List.of():accessContext.activatedGroups();List<ToolSchema>visibleTools=permissionResolver.filterVisibleToolSchemas(input.tools(),activatedGroups);ReasoningInputfilteredInput=newReasoningInput(input.messages(),visibleTools,input.options());returnnext.apply(filteredInput);}}

缺少权限快照时使用空权限集合,只保留公共工具,这是一种 fail-closed 策略。

即使未来增加新的非 HTTP 调用入口,只要调用方忘记设置权限快照,也不会默认暴露全部工具。

十一、为什么不动态修改单例 Toolkit

不建议针对每个用户调用共享 Toolkit 的全局工具激活方法。

原因是:

  1. HarnessAgent是单例。
  2. Toolkit也是该 Agent 的共享配置。
  3. 不同(userId, sessionId)可以并发运行。
  4. 如果把用户 A 的激活组写进共享 Toolkit,用户 B 可能覆盖该状态。

本文的实现不修改 Toolkit:

共享 Toolkit:始终保存完整工具定义 请求 A:生成 visibleTools(A) 请求 B:生成 visibleTools(B) 两份列表互不修改,也不写入共享状态

因此既保留了单例HarnessAgent,又实现了请求级 Schema 隔离。

十二、在 Runner 中写入权限快照

publicclassWaterMeterHarnessAgentRunner{privatefinalHarnessAgentharnessAgent;privatefinalAgentToolPermissionResolverpermissionResolver;@AutowiredpublicWaterMeterHarnessAgentRunner(HarnessAgentharnessAgent,AgentToolPermissionResolverpermissionResolver){this.harnessAgent=harnessAgent;this.permissionResolver=permissionResolver;}publicFlux<AgentEvent>streamEvents(UserMessageuserMessage,RuntimeContextruntimeContext){AgentToolAccessContextaccessContext=permissionResolver.resolveCurrentUserAccessContext();runtimeContext.put(AgentToolAccessContext.class,accessContext);returnharnessAgent.streamEvents(userMessage,runtimeContext);}}

权限快照必须在 HTTP 请求线程中生成。此时 Spring Security 登录上下文仍然有效。

十三、声明查询工具

@ComponentpublicclassWaterMeterOrderQueryTools{@AutowiredprivateWaterOrderServicewaterOrderService;@Tool(name="get_water_order",description="按订单编号查询一笔水费订单",readOnly=true)@AgentToolPermission("water-meter:order:query")@PreAuthorize("@ss.hasPermission('water-meter:order:query')")publicStringgetWaterOrder(@ToolParam(name="orderId",description="水费订单编号")LongorderId){if(orderId==null){return"订单编号不能为空。";}WaterOrderDOorder=waterOrderService.getAdminWaterOrder(orderId);returnorder==null?"未查询到该水费订单。":JsonUtils.toJsonString(order);}}

三个注解的职责分别是:

注解使用方作用
@ToolAgentScope生成工具名称、描述和参数 Schema
@AgentToolPermission自定义 Resolver/Middleware决定谁能在模型上下文中看到 Schema
@PreAuthorizeSpring Method Security工具执行时再次检查权限

十四、Spring AOP 代理与 AgentScope 反射

当工具方法带有@PreAuthorize时,Spring 通常会为工具 Bean 创建 AOP 代理。

这里会出现一个兼容问题:

  1. AgentScope 需要扫描目标类上的@Tool@ToolParam生成 Schema。
  2. 工具执行又必须调用 Spring 代理,才能触发@PreAuthorize
  3. 如果直接扫描代理类,代理生成的方法不一定保留目标方法的@Tool注解。
  4. 如果直接调用原始 target,又会绕过 Spring Method Security。

解决方案是自定义AgentTool适配器:

目标类方法 → 用于生成 AgentScope Tool Schema Spring 代理对象 → 用于真正执行工具 → 触发 @PreAuthorize、事务等 AOP

这可以概括为:

目标类产 Schema,Spring 代理做执行。

工具执行线程还需要短暂恢复请求中保存的认证快照和租户上下文,并在finally中清理,避免线程池身份串用。

十五、单例 HarnessAgent 配置

@Bean(destroyMethod="close")publicHarnessAgentwaterMeterHarnessAgent(AgentRagPropertiesproperties,AgentToolPermissionResolverpermissionResolver,AgentToolPermissionMiddlewarepermissionMiddleware,WaterMeterOrderQueryToolsqueryTools,WaterMeterOrderSettlementToolssettlementTools){Toolkittoolkit=newToolkit();// 启动时一次性注册全部工具。registerPermissionTools(toolkit,permissionResolver,queryTools,settlementTools);returnHarnessAgent.builder().name("water-meter-agent").sysPrompt("你是智能水表后台助手。").model(buildModel(properties)).toolkit(toolkit).middleware(permissionMiddleware).build();}

整个应用生命周期中只创建一个HarnessAgent。用户差异只存在于每次调用的RuntimeContext和过滤后的ReasoningInput.tools

十六、测试权限矩阵

可以准备三类后台角色:

角色对话权限订单查询权限异常结算权限模型可见业务工具
受限角色0
查询角色2
结算角色3

测试时应验证:

  1. 受限角色的模型请求中不存在订单工具名称、描述和参数 Schema。
  2. 查询角色只能看到订单查询工具。
  3. 结算角色可以看到查询和结算工具。
  4. 查询角色能够真实执行查询工具并通过@PreAuthorize
  5. 受限角色和结算角色并发请求时,各自的 Tool Schema 不会串权。
  6. 未附加权限快照的内部调用只能看到公共工具。

十七、常见错误

1. 只在工具执行阶段拦截

这只能防止执行,不能防止 Schema 泄露给模型。

2. 为每个用户创建一套 HarnessAgent

这会破坏官方推荐的单例使用方式,增加模型、状态存储、工具和 Middleware 的生命周期管理成本。

3. 把当前用户权限保存到单例 Middleware 字段

不同用户并发时会相互覆盖,属于严重的越权风险。

4. 直接修改共享 Toolkit 激活状态

请求级权限不应该写入共享可变配置。

5. 删除@PreAuthorize

Schema 隐藏不是执行授权的替代品。服务端必须保留执行期校验,避免模型伪造、历史 ToolCall 或程序错误绕过可见性控制。

6. 直接注册 Spring AOP 代理

可能导致 AgentScope 无法找到目标方法上的@Tool注解。需要同时兼顾目标类 Schema 和代理对象执行。

十八、总结

若依权限系统与 AgentScope Tool 的正确结合方式不是简单地把@PreAuthorize放到@Tool方法上,而是采用两层权限模型:

第一层:模型调用前 @AgentToolPermission → Resolver 建立权限与工具映射 → Middleware 过滤 Tool Schema 第二层:工具执行时 @PreAuthorize → Spring Method Security 再次校验

最终可以同时实现:

  1. 无权限用户看不到工具名称。
  2. 无权限用户看不到工具描述。
  3. 无权限用户看不到参数 JSON Schema。
  4. 无权限用户无法执行工具。
  5. HarnessAgentToolkit保持应用级单例。
  6. 不同用户、不同会话并发时权限互不污染。

这种实现不仅适用于水表订单,也可以扩展到客户查询、财务审批、设备控制、报表导出等任意若依菜单权限场景。

参考资料

  • AgentScope Java 快速开始:https://java.agentscope.io/v2/zh/docs/quickstart.html
  • AgentScope Java Middleware:https://java.agentscope.io/v2/zh/docs/building-blocks/middleware.html
  • AgentScope Java Tool:https://java.agentscope.io/v2/zh/docs/building-blocks/tool.html
  • Spring Security Method Security:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询