很多 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 里的“请谨慎操作”重要得多。


Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐