给你的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 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工作流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值