身份、能力发现与信任
正在读取学习记录…
身份回答“当前连接的服务是谁”,能力发现回答“它声明能做什么”,授权回答“当前任务允许它做什么”。信任需要这些信息相互核对,不能由一张漂亮的自我介绍代替。
一个服务叫“退款资料助手”,声明支持读取和退款支付,这能帮助调用者找到候选服务。可是相同名称可以被别人复制,能力声明也可以夸大。应用需要从可信登记与认证结果判断能否调用。
能力是能做,权限是获准做。一个工作人员会操作财务系统,不代表他在每笔订单上都有付款权限。给Agent也要保留这一区别,工牌不能靠自己画。
能读退款规则,为什么不能写退款
本地登记中,refund-reader对应身份service:refund-reader,允许的地址来源是https://refund.example,授予的能力只有refund.read,即读取退款资料。
服务的能力声明却包含refund.read和refund.write。应用先核对已认证身份,再核对地址来源,最后检查所需能力既已声明、又在登记授权中。因此读操作允许,写操作拒绝。
程序中的authenticated_subject代表连接层已经完成认证后得到的主体名称;固定例子直接传入它来观察门禁行为。网络上的证书、令牌或签名验证,需要由真实认证组件产生这个结果,不能从待核对声明中取同名字段来代替。
保存完整程序并运行
在自己的学习目录打开PowerShell,创建一个新的子目录:
mkdir l6-identity
cd l6-identity把下面完整代码保存为 identity_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""本地身份与能力门禁;认证结果作为独立输入。"""
from urllib.parse import urlparse
REGISTRY = {
"refund-reader": {
"subject": "service:refund-reader", "origin": "https://refund.example",
"grants": {"refund.read"}, "enabled": True,
}
}
ANNOUNCEMENT = {
"id": "refund-reader", "endpoint": "https://refund.example/query",
"skills": ["refund.read", "refund.write"],
}
def discover(announcement, authenticated_subject, requested_skill):
# 名字和技能来自待核对声明,身份来自已完成认证的连接信息。
entry = REGISTRY.get(announcement.get("id"))
if not entry or not entry["enabled"]:
raise PermissionError("服务未登记或已停用")
if authenticated_subject != entry["subject"]:
raise PermissionError("认证身份不匹配")
endpoint = urlparse(announcement.get("endpoint", ""))
origin = f"{endpoint.scheme}://{endpoint.netloc}"
if endpoint.scheme != "https" or origin != entry["origin"]:
raise PermissionError("服务地址不在信任登记中")
advertised = set(announcement.get("skills", []))
if requested_skill not in advertised:
raise ValueError("服务未声明所需能力")
if requested_skill not in entry["grants"]:
raise PermissionError("能力已声明,但执行权限不足")
return announcement["endpoint"]
def main():
address = discover(ANNOUNCEMENT, "service:refund-reader", "refund.read")
print("可信读取:" + address)
for subject, skill in [("service:impostor", "refund.read"),
("service:refund-reader", "refund.write")]:
try:
discover(ANNOUNCEMENT, subject, skill)
except PermissionError as error:
print(skill + ":" + str(error))
changed = dict(ANNOUNCEMENT, endpoint="https://attacker.example/query")
try:
discover(changed, "service:refund-reader", "refund.read")
except PermissionError as error:
print("换地址:" + str(error))
if __name__ == "__main__":
main()保存后执行 dir,确认能看到 identity_demo.py,再运行:
ls
python -X utf8 identity_demo.py程序输出:
可信读取:https://refund.example/query
refund.read:认证身份不匹配
refund.write:能力已声明,但执行权限不足
换地址:服务地址不在信任登记中三道检查为什么要分开
登记表REGISTRY由应用维护。announcement是服务提供的待核对能力描述,不能修改登记表。第一道检查使用服务id找到登记项,并判断enabled,表示当前是否仍允许使用它。
第二道检查比对authenticated_subject与登记中的subject。服务名称相同而认证主体不同,会得到“认证身份不匹配”。这能阻止只复制服务名称的候选混入已登记身份。
第三道检查解析endpoint,把协议与主机组成origin,并要求它等于登记值且为HTTPS。固定路径/query可以变化,但来源不能偷偷换成attacker.example;普通HTTP也被拒绝。地址验证与身份验证共同限定调用目标。
最后还要检查所需skill是否在能力声明中,并且在grants中。两者取交集,而不是把声明列表直接当权限列表。service被停用时,其他条件都成立也不会继续调用。
发现能力之后还要验证任务归属
能力发现通常会使用服务名片、登记目录或预先配置的地址。A2A的AgentCard提供可交互方式与技能等信息,调用者据此找候选服务。描述包含某项技能,不等于已完成身份验证,也不等于对当前客户数据拥有访问权。
例子只有一个受信任服务和读取权限。扩展到多租户时,授权还要带任务数据范围,例如只能读取shop-A的O-17,不能用同一read能力访问所有商家订单。权限判断应放在真正执行读取的服务端,再由调用端减少不必要的暴露。
代理调用还有另一个问题:主管有权限,不代表子Agent可以拿到主管的全部凭证。可以传递受限的任务授权,或让主管代为调用特定接口,再把必要结果给子Agent。凭证的范围、期限和撤销方式需要和委派任务一致。
哪些信息可以相信到什么程度
名称、描述和技能标签是可读线索。认证主体是连接经过验证后的身份。登记授权是应用决定的允许范围。历史运行表现则回答服务是否稳定、产物是否符合契约。它们支持不同判断,不应全部缩成一个“可信=True”。
HTTPS保护连接并帮助验证服务器地址,也不会证明服务的退款结论正确。通过门禁后,仍要检查返回对象、订单归属、资料版本与业务证据。可信身份可以出错,甚至读取过期规则。
登记也会过时。服务迁址、权限被收回、密钥撤销时,发现缓存应按合适策略刷新;不能因为昨天连通过,就永久跳过核对。缓存可减少开销,但失效和撤销需要明确路径。
改声明,看授权会不会跟着变
把ANNOUNCEMENT的skills增加refund.delete,再把main中("service:refund-reader", "refund.write")这组请求的技能改为refund.delete,运行python -X utf8 identity_demo.py。声明里有新能力,登记grants里没有,仍会显示执行权限不足。第一条refund.read调用保持原样。
先将新增的refund.delete声明删除,并把改过的请求恢复为refund.write。再把REGISTRY中的enabled改为False,运行同一命令,第一条正常读取就会报“服务未登记或已停用”。最终恢复enabled=True,重跑确认读取允许、写入拒绝,再运行配套检查。
若修改地址后失败,核对完整scheme与netloc;若同名服务无法通过,核对认证subject,而不是修改显示名称。若能力已声明但权限不足,需要改变正式授权配置,不能靠更强的角色提示词绕过去。
参考阅读:A2A Agent Discovery关注名片与候选服务发现;Authentication and Authorization区分客户端认证与服务端执行授权。
运行配套检查
把下面测试完整保存为 test_identity_demo.py,与主程序放在同一目录。6项检查覆盖正常读取、冒名身份、未授权写入、地址来源、停用登记和未声明技能。
查看测试代码:test_identity_demo.py
"""身份、地址和授予权限需要分别成立。"""
import copy
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import identity_demo as demo
class IdentityTests(unittest.TestCase):
def test_trusted_registered_read(self):
self.assertEqual(demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.read"),
"https://refund.example/query")
def test_same_name_different_subject_rejected(self):
with self.assertRaises(PermissionError):
demo.discover(demo.ANNOUNCEMENT, "service:impostor", "refund.read")
def test_claimed_write_does_not_grant_write(self):
with self.assertRaisesRegex(PermissionError, "权限不足"):
demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.write")
def test_changed_origin_and_plain_http_rejected(self):
for endpoint in ("https://attacker.example/query", "http://refund.example/query"):
with self.subTest(endpoint=endpoint), self.assertRaises(PermissionError):
demo.discover(dict(demo.ANNOUNCEMENT, endpoint=endpoint),
"service:refund-reader", "refund.read")
def test_revoked_registry_entry(self):
registry = copy.deepcopy(demo.REGISTRY)
registry["refund-reader"]["enabled"] = False
with patch.object(demo, "REGISTRY", registry), self.assertRaises(PermissionError):
demo.discover(demo.ANNOUNCEMENT, "service:refund-reader", "refund.read")
def test_unadvertised_skill_rejected(self):
with self.assertRaises(ValueError):
demo.discover(dict(demo.ANNOUNCEMENT, skills=[]), "service:refund-reader", "refund.read")
if __name__ == "__main__":
unittest.main()python -X utf8 -m unittest -v test_identity_demo.py测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 服务介绍宣称可以查询订单,应用还要核对什么
服务介绍宣称可以查询订单,应用还要核对什么?
对应知识点:L6-identity
简答题 · 解释机制
能力发现、认证和授权分别回答什么?
对应知识点:L6-identity
代码题 · 核对给定数据
用户owner与订单owner相同才可读,写一条布尔判断。
对应知识点:L6-identity
场景题 · 分析具体变化
两个执行者技能相同,一个只能读当前客户的订单,怎样选择?
对应知识点:L6-identity
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。