拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Boot实现Agent委托授权网关

很多 Agent 项目最开始的授权逻辑非常直接用户连接 Google / CRM / GitHub ↓ 系统拿到 OAuth Token ↓ Agent 需要 Tool 时直接使用PoC 阶段很方便。生产以后问题会越来越多Agent 代表哪个用户 这次调用为了哪个任务 为什么有权访问这个资源 权限能用多久 一个 Token 被几个 Agent 共享 高风险动作有没有二次批准所以我更倾向在 Agent 与外部 Tool 之间加一个Delegated Authority GatewayAgent 不直接持有长期 Token。它只声明我需要什么 Capability 为了什么 Purpose 要访问什么 Resource网关根据用户连接、组织角色、Agent Policy 和当前 Run签发一个短期 Execution Grant。目标架构User Connection ↓ Credential Vault ↓ Agent Run ↓ Delegated Authority Gateway ├─ Identity ├─ Purpose ├─ Capability Policy ├─ Resource Scope ├─ Approval └─ Audit ↓ Ephemeral Credential ↓ Tool / MCP / SaaS API长期 Credential 永远留在 Vault。Agent 只拿短期能力。Maven 依赖dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-oauth2-resource-server/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-jpa/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependencydependencygroupIdio.micrometer/groupIdartifactIdmicrometer-registry-prometheus/artifactId/dependency/dependencies这里没有强绑定某个 OAuth Provider。因为授权网关应该服务Google Microsoft GitHub Salesforce 内部系统 MCP第一层用户连接不是执行权限用户完成 OAuth 后保存的是 Connection。publicrecordUserConnection(StringconnectionId,StringtenantId,StringsubjectId,Stringprovider,SetStringgrantedScopes,StringcredentialRef,ConnectionStatusstatus,InstantconnectedAt,InstantexpiresAt){}credentialRef指向 Secret Vault。数据库里不要存明文 Refresh Token。状态publicenumConnectionStatus{ACTIVE,REAUTH_REQUIRED,REVOKED,EXPIRED}重点是Connection 只表示 “用户曾经连接了某个系统”不是“所有 Agent 永久可以使用这些权限”第二层Agent 声明 Capability而不是声明 Token请求publicrecordAuthorityRequest(StringrunId,StringstepId,StringagentId,StringtenantId,StringsubjectId,Stringpurpose,StringcapabilityId,ListStringresourceIds,JsonNodeactionSummary){}例如{runId:run-1842,stepId:step-7,agentId:sales-renewal-agent,tenantId:team-a,subjectId:user-82,purpose:renewal-risk-review,capabilityId:crm.customer.read,resourceIds:[customer/12345]}Agent 不需要知道OAuth Scope 是什么 Refresh Token 在哪 Access Token 怎么续这些属于 Gateway。第三层把 Purpose 变成正式字段publicrecordPurposeContext(StringpurposeId,StringrunId,StringtaskType,InstantcreatedAt,InstantexpiresAt,SetStringallowedCapabilities){}例如renewal-risk-review允许crm.customer.read contract.read support.ticket.read不允许crm.customer.delete email.bulk_send这比只看用户角色更细。Policy DecisionpublicrecordAuthorityDecision(booleanallowed,booleanapprovalRequired,SetStringproviderScopes,SetStringallowedResources,DurationcredentialTtl,intmaximumCalls,ListStringreasons,StringpolicyVersion){}不要只返回true / false生产里常见第三种状态可以做 但需要人批准一个简单 Policy EngineServicepublicclassDelegatedAuthorityPolicy{publicAuthorityDecisionevaluate(AuthorityRequestrequest,UserConnectionconnection,CapabilityPolicycapability){if(!connection.subjectId().equals(request.subjectId())){returnAuthorityDecisionFactory.deny(SUBJECT_MISMATCH);}if(!capability.allowedPurposes().contains(request.purpose())){returnAuthorityDecisionFactory.deny(PURPOSE_NOT_ALLOWED);}if(!capability.matchesResources(request.resourceIds())){returnAuthorityDecisionFactory.deny(RESOURCE_OUT_OF_SCOPE);}booleanapprovalcapability.riskLevel().compareTo(RiskLevel.HIGH)0;returnAuthorityDecisionFactory.allow(capability.providerScopes(),request.resourceIds(),Duration.ofMinutes(approval?5:15),capability.maxCallsPerRun(),approval,capability.policyVersion());}}实际系统还会接RBACABACTenant PolicyData ClassificationWork HoursRiskDeviceLocation。但核心流程一样。Capability PolicypublicrecordCapabilityPolicy(StringcapabilityId,RiskLevelriskLevel,SetStringallowedPurposes,SetStringproviderScopes,ListStringallowedResourcePatterns,intmaxCallsPerRun,StringpolicyVersion){publicbooleanmatchesResources(ListStringresources){returnresources.stream().allMatch(this::matches);}privatebooleanmatches(Stringresource){returnallowedResourcePatterns.stream().anyMatch(resource::startsWith);}}生产中不要直接用startsWith做安全匹配这里只是示意。真正应该使用标准化 Resource Identifier。Execution GrantPolicy 通过以后不立即给 Token。先生成publicrecordExecutionGrant(StringgrantId,StringrunId,StringstepId,StringagentId,StringsubjectId,StringtenantId,Stringpurpose,StringcapabilityId,SetStringresources,intremainingCalls,InstantexpiresAt,GrantStatusstatus){}状态publicenumGrantStatus{CREATED,WAITING_APPROVAL,ACTIVE,CONSUMED,REVOKED,EXPIRED}高风险 Grant 先停在审批例如email.send crm.update cloud.deploy payment.refund创建WAITING_APPROVAL审批内容不要只有是否允许应该显示Agent sales-agent 代表 张三 动作 向 customer/12345 发送续约邮件 目的 renewal-campaign-2026Q3 有效时间 5 分钟 最大调用 1 次用户知道自己在批准什么。Approval 绑定 Input Hash否则审批之后 Agent 改参数。publicrecordApprovalRecord(StringapprovalId,StringgrantId,StringactionHash,StringapprovedBy,InstantapprovedAt,InstantexpiresAt){}执行时当前 Action Hash 必须等于 批准时的 Hash如果收件人或金额变化重新批准Credential Broker真正需要调用 Provider 时publicinterfaceCredentialBroker{EphemeralCredentialissue(UserConnectionconnection,ExecutionGrantgrant,AuthorityDecisiondecision);}返回publicrecordEphemeralCredential(StringaccessToken,InstantexpiresAt,SetStringscopes,StringtokenFingerprint){}accessToken只活几分钟。最好只存在内存不写数据库和日志。为什么不要把 Refresh Token 给 AgentRefresh Token 能持续获取新 Access Token。如果 Agent 拿到它Run 结束也不代表权限结束。更安全的结构Agent 永远不知道 Refresh Token Gateway 按每次 Execution Grant 换短期 Access Token这样 Run 结束以后能力自然衰减。Tool ProxyAgent 不直接调用第三方 API。RestControllerRequestMapping(/api/tool)publicclassDelegatedToolController{privatefinalDelegatedToolServiceservice;PostMapping(/{capabilityId})publicToolResponseinvoke(PathVariableStringcapabilityId,RequestBodyToolInvocationRequestrequest,Authenticationauthentication){returnservice.invoke(capabilityId,request,authentication);}}Tool RequestpublicrecordToolInvocationRequest(StringrunId,StringstepId,StringgrantId,JsonNodearguments){}调用前再次校验 GrantTransactionalpublicToolResponseinvoke(StringcapabilityId,ToolInvocationRequestrequest,Authenticationauth){ExecutionGrantgrantgrantRepository.lockById(request.grantId());grantValidator.requireActive(grant,capabilityId,request.runId(),request.stepId());approvalValidator.requireValidIfNeeded(grant,request.arguments());if(grant.remainingCalls()0){thrownewGrantExhaustedException();}grant.consumeOneCall();EphemeralCredentialcredentialcredentialService.issue(grant);returnadapterRegistry.forCapability(capabilityId).invoke(credential,request.arguments());}使用数据库行锁或原子计数防止两个 Worker 同时消费最后一次授权。Token Fingerprint日志绝不写 Token。只写sha256(token)[0:12]或者 Provider 返回的 Token ID。目的只是关联这次调用用了哪次临时凭证不是保存 Secret。Audit EventpublicrecordAuthorityAuditEvent(StringeventId,StringrunId,StringstepId,StringagentId,StringsubjectId,StringtenantId,Stringpurpose,StringcapabilityId,StringresourceHash,StringgrantId,StringapprovalId,Stringdecision,StringpolicyVersion,Instanttimestamp){}以后能回答谁授权 Agent 代表谁 为了什么 调用了什么一个重要原则User Connection 和 Agent Grant 分离Connection 可能存在90 天Execution Grant 只存在5 分钟用户不需要每次重新登录。Agent 也不会因此获得 90 天无限权限。这个结构很适合Connected App Agent RuntimeResource Scope别只给drive.read尽量继续缩folder/abc甚至file/123例如publicrecordResourceConstraint(StringresourceType,SetStringresourceIds,SetStringallowedFields){}如果 SaaS Provider 自己不支持字段级权限Gateway 至少可以在响应侧做数据裁剪。Response Filtering 也属于授权很多团队只限制请求。其实读取 Tool 返回后也应该检查是否返回了超出 Resource Scope 的数据例如 CRM API 一次返回整个客户对象。Agent 只需要name stage recent_activityGateway 可以过滤{name:...,stage:...,recent_activity:[...]}而不把billing private_notes personal_phone全部交给模型。Delegated Authority 还需要 Data Classification例如PUBLIC INTERNAL CONFIDENTIAL RESTRICTEDCapability Policy 可以规定普通 Agent 最高 CONFIDENTIAL 代码沙箱 最高 INTERNAL 高安全 Agent 需要额外批准才能读取 RESTRICTED权限不只是“能不能调 Tool”。还包括能看什么数据Grant Revocation必须支持publicvoidrevokeByRun(StringrunId){grantRepository.revokeActiveByRun(runId);}以及revokeByUser revokeByAgent revokeByConnection revokeByCapability revokeByTenant例如员工离职User Disabled ↓ Revoke Connections ↓ Revoke Active Grants角色变化也要触发 Re-evaluationHR 系统发RoleChangedAuthority Service 检查现有 Connection 是否仍允许 现有 Grant 是否仍允许不要等 Token 自然过期几个月。Metricsdelegated_grant_total{ decision, capability } delegated_grant_active delegated_approval_total{ result } delegated_credential_issued_total{ provider } delegated_invocation_denied_total{ reason } delegated_grant_revoked_total{ reason }高基数的 user/run 不放普通 Metric放 Trace。Traceagent.run └─authority.request ├─connection.resolve ├─policy.evaluate ├─approval.wait ├─credential.issue └─tool.invoke自动化测试最少覆盖这些用户 A 不能使用用户 B Connection Agent 超出 Purpose 被拒绝 Resource 超范围被拒绝 Grant 过期后调用失败 Grant 调用次数耗尽 审批后参数变化重新审批 Connection 撤销后 Grant 失效 并发调用不会超用 Grant 日志不出现 Access/Refresh Token一个很关键的故障策略Authority Service 挂了怎么办低风险只读任务可以考虑使用短期已签名 Grant高风险写任务Fail Closed不要因为授权服务不可用就临时绕过授权。为什么 Signed Grant 有价值Gateway 可以签一个短期 JWT{run:run-1842,agent:sales-agent,sub:user-82,cap:crm.customer.read,resources:[customer/12345],purpose:renewal-review,exp:1780000000}Tool Gateway 本地验证签名不需要每个调用都查中央数据库。这可以降低授权服务成为全局瓶颈的风险。高风险 Capability 仍然可以强制在线校验。最后的生产边界我会坚持四点1. Agent 不拥有长期用户 Token 2. Connection 不等于 Execution Grant 3. Grant 必须绑定 Purpose 和 Resource 4. 高风险动作绑定 Approval 和 Input Hash这四条一旦建立OAuth、MCP、SaaS Tool 和内部 API 都可以挂在同一个授权模型下面。很多 Agent 身份问题看起来很复杂其实核心不是重新发明 OAuth。真正要补的是传统 OAuth 很少直接表达的三件事这次任务为什么需要权限 这次只允许访问哪些资源 这个 Agent 可以把授权用多久所以 Delegated Authority Gateway 的价值不是再包一层 API。而是把用户长期连接转换成Agent 本次任务真正需要的最小、短期、可审计权限当 Agent 开始能发邮件、改 CRM、操作云资源、访问企业文件以后这一层会比 Prompt 里的“请谨慎操作”重要得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门