Spring Boot实现Agent委托授权网关
很多 Agent 项目最开始的授权逻辑非常直接:
用户连接 Google / CRM / GitHub
↓
系统拿到 OAuth Token
↓
Agent 需要 Tool 时直接使用
PoC 阶段很方便。
生产以后,问题会越来越多:
Agent 代表哪个用户?
这次调用为了哪个任务?
为什么有权访问这个资源?
权限能用多久?
一个 Token 被几个 Agent 共享?
高风险动作有没有二次批准?
所以我更倾向在 Agent 与外部 Tool 之间加一个:
Delegated Authority Gateway
Agent 不直接持有长期 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 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
这里没有强绑定某个 OAuth Provider。
因为授权网关应该服务:
Google
Microsoft
GitHub
Salesforce
内部系统
MCP
第一层:用户连接不是执行权限
用户完成 OAuth 后,保存的是 Connection。
public record UserConnection(
String connectionId,
String tenantId,
String subjectId,
String provider,
Set<String> grantedScopes,
String credentialRef,
ConnectionStatus status,
Instant connectedAt,
Instant expiresAt) {
}
credentialRef 指向 Secret Vault。
数据库里不要存明文 Refresh Token。
状态:
public enum ConnectionStatus {
ACTIVE,
REAUTH_REQUIRED,
REVOKED,
EXPIRED
}
重点是:
Connection 只表示
“用户曾经连接了某个系统”
不是:
“所有 Agent 永久可以使用这些权限”
第二层:Agent 声明 Capability,而不是声明 Token
请求:
public record AuthorityRequest(
String runId,
String stepId,
String agentId,
String tenantId,
String subjectId,
String purpose,
String capabilityId,
List<String> resourceIds,
JsonNode actionSummary) {
}
例如:
{
"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 变成正式字段
public record PurposeContext(
String purposeId,
String runId,
String taskType,
Instant createdAt,
Instant expiresAt,
Set<String> allowedCapabilities) {
}
例如:
renewal-risk-review
允许:
crm.customer.read
contract.read
support.ticket.read
不允许:
crm.customer.delete
email.bulk_send
这比只看用户角色更细。
Policy Decision
public record AuthorityDecision(
boolean allowed,
boolean approvalRequired,
Set<String> providerScopes,
Set<String> allowedResources,
Duration credentialTtl,
int maximumCalls,
List<String> reasons,
String policyVersion) {
}
不要只返回:
true / false
生产里常见第三种状态:
可以做
但需要人批准
一个简单 Policy Engine
@Service
public class DelegatedAuthorityPolicy {
public AuthorityDecision evaluate(
AuthorityRequest request,
UserConnection connection,
CapabilityPolicy capability) {
if (!connection.subjectId()
.equals(request.subjectId())) {
return AuthorityDecisionFactory.deny(
"SUBJECT_MISMATCH");
}
if (!capability.allowedPurposes()
.contains(request.purpose())) {
return AuthorityDecisionFactory.deny(
"PURPOSE_NOT_ALLOWED");
}
if (!capability.matchesResources(
request.resourceIds())) {
return AuthorityDecisionFactory.deny(
"RESOURCE_OUT_OF_SCOPE");
}
boolean approval =
capability.riskLevel()
.compareTo(RiskLevel.HIGH) >= 0;
return AuthorityDecisionFactory.allow(
capability.providerScopes(),
request.resourceIds(),
Duration.ofMinutes(
approval ? 5 : 15),
capability.maxCallsPerRun(),
approval,
capability.policyVersion());
}
}
实际系统还会接:
- RBAC;
- ABAC;
- Tenant Policy;
- Data Classification;
- Work Hours;
- Risk;
- Device;
- Location。
但核心流程一样。
Capability Policy
public record CapabilityPolicy(
String capabilityId,
RiskLevel riskLevel,
Set<String> allowedPurposes,
Set<String> providerScopes,
List<String> allowedResourcePatterns,
int maxCallsPerRun,
String policyVersion) {
public boolean matchesResources(
List<String> resources) {
return resources.stream()
.allMatch(this::matches);
}
private boolean matches(
String resource) {
return allowedResourcePatterns
.stream()
.anyMatch(resource::startsWith);
}
}
生产中不要直接用 startsWith 做安全匹配,这里只是示意。
真正应该使用标准化 Resource Identifier。
Execution Grant
Policy 通过以后,不立即给 Token。
先生成:
public record ExecutionGrant(
String grantId,
String runId,
String stepId,
String agentId,
String subjectId,
String tenantId,
String purpose,
String capabilityId,
Set<String> resources,
int remainingCalls,
Instant expiresAt,
GrantStatus status) {
}
状态:
public enum GrantStatus {
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 改参数。
public record ApprovalRecord(
String approvalId,
String grantId,
String actionHash,
String approvedBy,
Instant approvedAt,
Instant expiresAt) {
}
执行时:
当前 Action Hash
必须等于
批准时的 Hash
如果收件人或金额变化:
重新批准
Credential Broker
真正需要调用 Provider 时:
public interface CredentialBroker {
EphemeralCredential issue(
UserConnection connection,
ExecutionGrant grant,
AuthorityDecision decision);
}
返回:
public record EphemeralCredential(
String accessToken,
Instant expiresAt,
Set<String> scopes,
String tokenFingerprint) {
}
accessToken 只活几分钟。
最好只存在内存,不写数据库和日志。
为什么不要把 Refresh Token 给 Agent
Refresh Token 能持续获取新 Access Token。
如果 Agent 拿到它:
Run 结束
也不代表权限结束。
更安全的结构:
Agent
永远不知道 Refresh Token
Gateway
按每次 Execution Grant
换短期 Access Token
这样 Run 结束以后,能力自然衰减。
Tool Proxy
Agent 不直接调用第三方 API。
@RestController
@RequestMapping("/api/tool")
public class DelegatedToolController {
private final DelegatedToolService service;
@PostMapping("/{capabilityId}")
public ToolResponse invoke(
@PathVariable String capabilityId,
@RequestBody ToolInvocationRequest request,
Authentication authentication) {
return service.invoke(
capabilityId,
request,
authentication);
}
}
Tool Request:
public record ToolInvocationRequest(
String runId,
String stepId,
String grantId,
JsonNode arguments) {
}
调用前再次校验 Grant
@Transactional
public ToolResponse invoke(
String capabilityId,
ToolInvocationRequest request,
Authentication auth) {
ExecutionGrant grant =
grantRepository.lockById(
request.grantId());
grantValidator.requireActive(
grant,
capabilityId,
request.runId(),
request.stepId());
approvalValidator.requireValidIfNeeded(
grant,
request.arguments());
if (grant.remainingCalls() <= 0) {
throw new GrantExhaustedException();
}
grant.consumeOneCall();
EphemeralCredential credential =
credentialService.issue(grant);
return adapterRegistry
.forCapability(capabilityId)
.invoke(
credential,
request.arguments());
}
使用数据库行锁或原子计数,防止两个 Worker 同时消费最后一次授权。
Token Fingerprint
日志绝不写 Token。
只写:
sha256(token)[0:12]
或者 Provider 返回的 Token ID。
目的只是关联:
这次调用用了哪次临时凭证
不是保存 Secret。
Audit Event
public record AuthorityAuditEvent(
String eventId,
String runId,
String stepId,
String agentId,
String subjectId,
String tenantId,
String purpose,
String capabilityId,
String resourceHash,
String grantId,
String approvalId,
String decision,
String policyVersion,
Instant timestamp) {
}
以后能回答:
谁授权
Agent 代表谁
为了什么
调用了什么
一个重要原则:User Connection 和 Agent Grant 分离
Connection 可能存在:
90 天
Execution Grant 只存在:
5 分钟
用户不需要每次重新登录。
Agent 也不会因此获得 90 天无限权限。
这个结构很适合:
Connected App
+
Agent Runtime
Resource Scope
别只给:
drive.read
尽量继续缩:
folder/abc
甚至:
file/123
例如:
public record ResourceConstraint(
String resourceType,
Set<String> resourceIds,
Set<String> allowedFields) {
}
如果 SaaS Provider 自己不支持字段级权限,Gateway 至少可以在响应侧做数据裁剪。
Response Filtering 也属于授权
很多团队只限制请求。
其实读取 Tool 返回后,也应该检查:
是否返回了超出 Resource Scope 的数据
例如 CRM API 一次返回整个客户对象。
Agent 只需要:
name
stage
recent_activity
Gateway 可以过滤:
{
"name": "...",
"stage": "...",
"recent_activity": [...]
}
而不把:
billing
private_notes
personal_phone
全部交给模型。
Delegated Authority 还需要 Data Classification
例如:
PUBLIC
INTERNAL
CONFIDENTIAL
RESTRICTED
Capability Policy 可以规定:
普通 Agent:
最高 CONFIDENTIAL
代码沙箱:
最高 INTERNAL
高安全 Agent:
需要额外批准才能读取 RESTRICTED
权限不只是“能不能调 Tool”。
还包括:
能看什么数据
Grant Revocation
必须支持:
public void revokeByRun(
String runId) {
grantRepository.revokeActiveByRun(runId);
}
以及:
revokeByUser
revokeByAgent
revokeByConnection
revokeByCapability
revokeByTenant
例如员工离职:
User Disabled
↓
Revoke Connections
↓
Revoke Active Grants
角色变化也要触发 Re-evaluation
HR 系统发:
RoleChanged
Authority Service 检查:
现有 Connection 是否仍允许
现有 Grant 是否仍允许
不要等 Token 自然过期几个月。
Metrics
delegated_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。
Trace
agent.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 里的“请谨慎操作”重要得多。
更多推荐


所有评论(0)