给你的A2A-Agent加把锁-认证鉴权实战指南

给你的A2A Agent加把锁:认证鉴权实战指南

摘要: A2A Agent暴露到网络上等于裸奔。手把手实现API Key、JWT Bearer、OAuth 2.0三种认证方式,从开发到生产的完整安全方案。

一、上篇的Agent有个大问题

上篇你搭了翻译Agent和摘要Agent,两个Agent在本地跑得好好的。但有个问题你可能没注意到:任何知道你端口号的人都能调用你的Agent

没有认证、没有鉴权、没有审计。你的Agent在10002和10003端口上"裸奔",谁都能发个JSON-RPC请求过来,让你的Agent干活。

在本地demo里这不是问题。但一旦你把Agent部署到服务器上------特别是公网环境------这就是一个安全漏洞。别人可以恶意调用你的Agent消耗算力、窃取数据、或者篡改任务结果。

A2A协议原生支持认证鉴权。它的做法很聪明:把认证信息放在Agent Card里声明,客户端看到声明后就知道该怎么认证。

今天带你实现三种认证方式,从简到繁。

二、A2A认证的底层逻辑

在写代码之前,先搞清楚A2A认证是怎么工作的。

两个关键原则

原则1:认证在HTTP层,不在JSON-RPC里。 A2A的JSON-RPC payload里不包含任何认证信息。Token、API Key这些东西通过HTTP Header传递(比如 Authorization: Bearer xxxX-API-Key: xxx)。

原则2:Agent Card声明认证需求。 Agent在自己的Card里通过 securitySchemes 字段告诉客户端:"我需要什么类型的认证、凭证放在哪个Header里"。客户端读取Card后就知道该怎么准备凭证。

三级认证方案

| 级别 | 方式 | 适用场景 | 安全等级 | | --- | --- | --- | --- | | Level 1 | API Key | 内部Agent、原型开发 | 低 | | Level 2 | JWT Bearer | 多团队、有身份提供商 | 中 | | Level 3 | OAuth 2.0 | 跨组织、合规要求 | 高 |

三级递进,不是互斥的。你可以从Level 1开始,需要时升级到Level 2或3,不需要重写Agent。

三、Level 1:API Key认证(5分钟搞定)

3.1 Agent Card声明

在Agent Card里加上 securitySchemes 字段:

bash 复制代码
agent_card = AgentCard(
    name="Protected Agent",
    description="An agent with API Key authentication",
    url=f"http://{host}:{port}/",
    version="0.1.0",
    defaultInputModes=["text"],
    defaultOutputModes=["text"],
    capabilities=AgentCapabilities(streaming=True),
    skills=[skill],
    securitySchemes={
"apiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
        }
    },
    security=[{"apiKey": []}],
)

这段声明告诉客户端:我要你在请求头里带一个 X-API-Key 字段。

3.2 写一个认证中间件

用Starlette中间件拦截每个请求,校验API Key:

bash 复制代码
import os
import hashlib
from 
            starlette.requests
           import Request
from 
            starlette.responses
           import JSONResponse
from 
            starlette.middleware.base
           import BaseHTTPMiddleware


classAPIKeyMiddleware(BaseHTTPMiddleware):
"""API Key认证中间件。"""

# 存储API Key的SHA-256哈希(别存明文!)
    VALID_KEYS = {
        hashlib.sha256(
            
            os.environ.
          get("AGENT_API_KEY", "dev-key-123").encode()
        ).hexdigest(): "trusted-client",
    }

asyncdefdispatch(self, request: Request, call_next):
# Agent Card始终公开,不需要认证
if 
            request.url.path
           == "/.well-known/
            agent.json"
          :
returnawaitcall_next(request)

# 检查X-API-Key头
        api_key = 
            request.headers.
          get("X-API-Key")
ifnot api_key:
returnJSONResponse(
                status_code=401,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Missing X-API-Key header",
                    },
                },
            )

# 校验Key
        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        client_id = self.VALID_KEYS.get(key_hash)
ifnot client_id:
returnJSONResponse(
                status_code=401,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Invalid API key",
                    },
                },
            )

# 把client_id存到
            request.state里,后面可以用
          
        
            request.state.client_id
           = client_id
returnawaitcall_next(request)

关键细节:Agent Card的端点 /.well-known/ [agent.json](http://agent.json) 必须跳过认证。否则客户端连你的Agent Card都拿不到,怎么知道需要什么认证?

3.3 把中间件挂到Server上

bash 复制代码
from 
            starlette.applications
           import Starlette
from 
            starlette.routing
           import Route

# 创建Starlette应用,挂载中间件
app = Starlette(
    routes=[
Route(
"/.well-known/
            agent.json"
          ,
            agent_card_handler,
        ),
Route("/", a2a_handler, methods=["POST"]),
    ],
)
app.add_middleware(APIKeyMiddleware)

# 用uvicorn跑Starlette应用
import uvicorn
uvicorn.run(app, host=host, port=port)

3.4 客户端带Key调用

bash 复制代码
asyncwith httpx.AsyncClient() as client:
    resp = await client.post(
        "http://localhost:10002/",
        json=payload,
        headers={"X-API-Key": "dev-key-123"},
    )

一个Header就搞定。

3.5 API Key适合什么场景

内部服务、原型开发、自己控制的Agent之间调用。它的优点是简单,缺点也很明显:

  • 所有客户端共享同一个Key,无法区分"谁调的"

  • Key泄漏了没法追溯

  • 没有权限粒度------有Key就能干所有事 如果你有这些需求,升级到Level 2。

四、Level 2:JWT Bearer认证(多团队协作)

4.1 Agent Card声明

bash 复制代码
securitySchemes={
"bearer": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
    }
},
security=[{"bearer": []}],

4.2 JWT验证中间件

JWT的好处:Token本身携带身份信息和权限(claims),Server不需要查数据库就能验证。

bash 复制代码
import jwt
from jwt import PyJWKClient
from 
            starlette.requests
           import Request
from 
            starlette.responses
           import JSONResponse
from 
            starlette.middleware.base
           import BaseHTTPMiddleware


classJWTMiddleware(BaseHTTPMiddleware):
def__init__(self, app, issuer: str, jwks_url: str, audience: str):
super().__init__(app)
self.issuer = issuer
self.audience = audience
# JWKS客户端,用于获取签名公钥
self.jwks_client = PyJWKClient(
            jwks_url, cache_keys=True
        )

asyncdefdispatch(self, request: Request, call_next):
# Agent Card公开
if 
            request.url.path
           == "/.well-known/
            agent.json"
          :
returnawaitcall_next(request)

# 提取Bearer Token
        auth_header = 
            request.headers.
          get("Authorization", "")
ifnot auth_header.startswith("Bearer "):
returnJSONResponse(
                status_code=401,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Missing Bearer token",
                    },
                },
            )

        token = auth_header[7:]  # 去掉"Bearer "

try:
# 获取签名公钥
            signing_key = self.jwks_client.get_signing_key_from_jwt(token)
# 验证Token
            claims = jwt.decode(
                token,
                
            signing_key.key,
          
                algorithms=["RS256", "ES256"],
                audience=self.audience,
                issuer=self.issuer,
                options={
"require": ["exp", "iss", "aud", "sub"]
                },
            )
except 
            jwt.ExpiredSignatureError:
          
returnJSONResponse(
                status_code=401,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Token expired",
                    },
                },
            )
except 
            jwt.InvalidTokenError
           as e:
returnJSONResponse(
                status_code=401,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": f"Invalid token: {e}",
                    },
                },
            )

# 把claims存到request里
        
            request.state.claims
           = claims
        
            request.state.client_id
           = claims.get("sub", "unknown")
returnawaitcall_next(request)

4.3 权限检查(Scope)

JWT可以携带权限范围(scope)。比如你的Agent有多个技能,你可以规定只有带 agent:execute scope的Token才能执行任务:

bash 复制代码
defrequire_scope(required_scope: str):
"""检查JWT中是否包含指定scope。"""
defchecker(request: Request):
        claims = getattr(
            request.state,
           "claims", {})
        scopes = claims.get("scope", "").split()
if required_scope notin scopes:
returnJSONResponse(
                status_code=403,
                content={
"jsonrpc": "2.0",
"error": {
"code": -32003,
"message": f"Missing scope: {required_scope}",
                    },
                },
            )
returnNone
return checker


# 在请求处理中使用
asyncdefhandle_task(request: Request):
error = require_scope("agent:execute")(request)
iferror:
returnerror
# 正常处理任务...

4.4 客户端带Token调用

bash 复制代码
import httpx

asyncdefcall_agent(agent_url: str, text: str, token: str):
    payload = {
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"type": "text", "text": text}],
            }
        },
    }
asyncwith httpx.AsyncClient() as client:
        resp = await client.post(
            agent_url,
            json=payload,
            headers={"Authorization": f"Bearer {token}"},
            timeout=60,
        )
        resp.raise_for_status()
return resp.json()

五、Level 3:OAuth 2.0 Client Credentials(生产级方案)

当你有多个Agent跨组织协作时,需要的是完整的OAuth 2.0流程。

5.1 Agent Card声明

bash 复制代码
securitySchemes={
"oauth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "https://
            auth.example.com/oauth2/token",
          
"scopes": {
"agent:read": "读取任务状态和元数据",
"agent:execute": "提交和管理任务",
"agent:admin": "配置Agent设置",
                }
            }
        }
    }
},
security=[{"oauth2": ["agent:execute"]}],

注意 security 字段里的 ["agent:execute"]------这意味着默认需要 agent:execute scope才能调用这个Agent。

5.2 能力级别细粒度控制

如果你某个技能需要更高的权限,可以在Skill级别覆盖安全声明:

bash 复制代码
skills=[
AgentSkill(
id="public-skill",
        name="Hello",
        description="Says hello",
        tags=["greeting"],
    ),
AgentSkill(
id="sensitive-skill",
        name="Data Analysis",
        description="Analyzes sensitive data",
        tags=["analysis", "pii"],
        security=[{"oauth2": ["agent:execute", "data:pii"]}],
    ),
]

data:pii skill需要额外的权限才能调用。

5.3 客户端自动获取Token

生产环境里,Token有有效期(一般15分钟),客户端需要自动刷新:

bash 复制代码
import time
import httpx


class OAuth2A2AClient:
"""自动管理OAuth 2.0 Token的A2A客户端。"""

def__init__(
self,
        token_url: str,
        client_id: str,
        client_secret: str,
        default_scopes: list[str] | None = None,
    ):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.default_scopes = default_scopes or ["agent:execute"]
self._token: str | None = None
self._token_expiry: float = 0

asyncdef_fetch_token(self, scopes: list[str]) -> str:
"""向授权服务器请求Token。"""
asyncwith httpx.AsyncClient() as http:
            resp = await http.post(
self.token_url,
                data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": " ".join(scopes),
                },
            )
            resp.raise_for_status()
            data = resp.json()
self._token = data["access_token"]
# 提前60秒刷新
self._token_expiry = (
                time.time()
                + data.get("expires_in", 3600)
                - 60
            )
returnself._token

asyncdefget_token(self, scopes: list[str] | None = None) -> str:
"""获取有效Token,过期自动刷新。"""
        scopes = scopes orself.default_scopes
ifself._token and time.time() < self._token_expiry:
returnself._token
returnawaitself._fetch_token(scopes)

asyncdefdiscover_and_call(
self, base_url: str, text: str
    ) -> dict:
"""完整流程:发现Agent→获取Token→调用。"""
asyncwith httpx.AsyncClient() as http:
# 1. 获取Agent Card
            card_resp = await http.get(
                f"{base_url}/.well-known/
            agent.json"
          
            )
            card = card_resp.json()

# 2. 从Card里提取认证需求
            oauth = card.get(
"securitySchemes", {}
            ).get("oauth2", {})
            flows = oauth.get("flows", {})
            cc = flows.get("clientCredentials", {})
self.token_url = cc.get(
"tokenUrl", self.token_url
            )

# 3. 获取所需scope
            security = card.get("security", [{}])
            required = security[0].get(
"oauth2", self.default_scopes[:1]
            )

# 4. 获取Token
            token = awaitself.get_token(required)

# 5. 调用Agent
            payload = {
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
                            {"type": "text", "text": text}
                        ],
                    }
                },
            }
            resp = await http.post(
                card["url"],
                json=payload,
                headers={
"Authorization": f"Bearer {token}"
                },
                timeout=60,
            )
            resp.raise_for_status()
return resp.json()

discover_and_call 方法展示了一个完整的自动化流程:读Agent Card→提取认证需求→获取Token→调用Agent。这就是一个合格的Agent编排器该做的事。

5.4 验证OAuth Token

OAuth 2.0 Client Credentials颁发的Token其实就是JWT。所以Level 2的JWT中间件直接复用,不需要额外代码。区别只在于Token的来源:Level 2是客户端自己签发的,Level 3是授权服务器颁发的。

六、三种方案怎么选

一个简单的决策树:

你的Agent只在内部用? → API Key。5分钟搞定,够用了。

多个团队共享Agent? → JWT Bearer。你有现成的身份提供商(比如Auth0、Keycloak),直接用。

Agent跨组织协作?合规要求高? → OAuth 2.0 Client Credentials。有授权服务器、有审计日志、有权限粒度。

我的建议:先用API Key跑通demo,验证业务逻辑没问题了,再切换到JWT或OAuth 2.0。别一开始就搞复杂的认证,业务逻辑改来改去的时候还要同时调认证代码,两头跑。

七、安全清单(部署前检查)

部署Agent到生产环境之前,过一遍这个清单:

  • 所有通信走HTTPS(TLS 1.3+)

  • Agent Card不包含明文密钥

  • API Key存哈希,不存明文

  • Token有效期≤15分钟(机器间通信)

  • 日志里记录client_id和scope,不记录Token

  • 定义了最小权限的scope(agent:read / agent:execute / agent:admin)

  • 凭证放在环境变量或密钥管理器里,不写进代码

  • Agent Card端点跳过认证(否则客户端发现不了你)

八、我踩过的3个坑

坑1:Agent Card端点加了认证。 客户端访问 /.well-known/ [agent.json](http://agent.json) 拿到401,直接报错说"无法发现Agent"。排查了20分钟才发现中间件拦截了这个路径。解决方法:中间件里加个条件判断,跳过Agent Card路径。

坑2:API Key存了明文。 写demo时图省事,把API Key直接写在代码里。后来代码传到Git上,被安全扫描工具告警了。解决方法:用环境变量,并且只存哈希值。

坑3:OAuth Token过期没处理。 第一次写的时候Token过期后客户端直接报错,没有自动刷新逻辑。生产环境里Token一般15分钟过期,如果不做自动刷新,Agent之间的通信会频繁中断。解决方法:用上面那个 OAuth2A2AClient 类,在Token过期前自动刷新。

下篇预告

《MCP管工具,A2A管协作:双协议联合实战》 ------ 让Agent既能通过MCP调外部工具,又能通过A2A跟其他Agent对话,搭一个"搜索+总结"的多Agent工作流。

相关推荐
我叫黑大帅1 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries1 小时前
文件上传系统
后端
止语Lab2 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活2 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困2 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn2 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530142 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908402 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金2 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛3 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构