给你的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 xxx 或 X-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 字段:
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:
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上
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调用
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声明
securitySchemes={
"bearer": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
}
},
security=[{"bearer": []}],
4.2 JWT验证中间件
JWT的好处:Token本身携带身份信息和权限(claims),Server不需要查数据库就能验证。
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才能执行任务:
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调用
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声明
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级别覆盖安全声明:
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分钟),客户端需要自动刷新:
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工作流。
2734

被折叠的 条评论
为什么被折叠?



