不要把用户 OAuth Token 直接塞给 Agent:用 Spring Boot 做一个 Delegated Authority Gateway
很多 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 依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-actuator
io.micrometer
micrometer-registry-prometheus
这里没有强绑定某个 OAuth Provider。
因为授权网关应该服务:
Google
Microsoft
GitHub
Salesforce
内部系统
MCP
第一层:用户连接不是执行权限
用户完成 OAuth 后,保存的是 Connection。
public record UserConnection(
String connectionId,
String tenantId,
String subjectId,
String provider,
Set 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 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 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 providerScopes,
Set allowedResources,
Duration credentialTtl,
int maximumCalls,
List 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 allowedPurposes,
Set providerScopes,
List allowedResourcePatterns,
int maxCallsPerRun,
String policyVersion) {
public boolean matchesResources(
List 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 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 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() resourceIds,
Set 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 里的“请谨慎操作”重要得多。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/