Skip to main content
在本教程中,你将扩展上一个教程中创建的聊天机器人,为每个用户提供独立的私密对话。你将添加资源级访问控制,使用户只能看到自己的对话线程。 授权流程:认证后,授权处理器为每个资源标记 owner=用户ID,并返回过滤器,使用户只能看到自己的对话线程。

前提条件

开始本教程前,请确保第一个教程中的机器人能够正常运行且无错误。

1. 添加资源授权

回想一下,在上一个教程中,Auth 对象允许你注册一个认证函数,LangSmith 用它来验证传入请求中的承载令牌。现在你将使用它来注册一个授权处理器。 授权处理器是在认证成功后运行的函数。这些处理器可以为资源添加元数据(例如资源的所有者),并过滤每个用户能看到的内容。 更新你的 src/security/auth.py 文件,添加一个在每个请求上运行的授权处理器:
src/security/auth.py
from langgraph_sdk import Auth

# 保留上一个教程中的测试用户
VALID_TOKENS = {
    "user1-token": {"id": "user1", "name": "Alice"},
    "user2-token": {"id": "user2", "name": "Bob"},
}

auth = Auth()


@auth.authenticate
async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserDict:
    """来自上一个教程的认证处理器。"""
    assert authorization
    scheme, token = authorization.split()
    assert scheme.lower() == "bearer"

    if token not in VALID_TOKENS:
        raise Auth.exceptions.HTTPException(status_code=401, detail="Invalid token")

    user_data = VALID_TOKENS[token]
    return {
        "identity": user_data["id"],
    }


@auth.on
async def add_owner(
    ctx: Auth.types.AuthContext,  # 包含当前用户的信息
    value: dict,  # 正在创建/访问的资源
):
    """使资源仅对其创建者可见。"""
    # 示例:
    # ctx: AuthContext(
    #     permissions=[],
    #     user=ProxyUser(
    #         identity='user1',
    #         is_authenticated=True,
    #         display_name='user1'
    #     ),
    #     resource='threads',
    #     action='create_run'
    # )
    # value:
    # {
    #     'thread_id': UUID('1e1b2733-303f-4dcd-9620-02d370287d72'),
    #     'assistant_id': UUID('fe096781-5601-53d2-b2f6-0d3403f7e9ca'),
    #     'run_id': UUID('1efbe268-1627-66d4-aa8d-b956b0f02a41'),
    #     'status': 'pending',
    #     'metadata': {},
    #     'prevent_insert_if_inflight': True,
    #     'multitask_strategy': 'reject',
    #     'if_not_exists': 'reject',
    #     'after_seconds': 0,
    #     'kwargs': {
    #         'input': {'messages': [{'role': 'user', 'content': 'Hello!'}]},
    #         'command': None,
    #         'config': {
    #             'configurable': {
    #                 'langgraph_auth_user': ... 你的用户对象...
    #                 'langgraph_auth_user_id': 'user1'
    #             }
    #         },
    #         'stream_mode': ['values'],
    #         'interrupt_before': None,
    #         'interrupt_after': None,
    #         'webhook': None,
    #         'feedback_keys': None,
    #         'temporary': False,
    #         'subgraphs': False
    #     }
    # }

    # 做两件事:
    # 1. 将用户ID添加到资源的元数据中。每个LangGraph资源都有一个 `metadata` 字典,随资源持久化。
    # 这个元数据在读取和更新操作中用于过滤非常有用。
    # 2. 返回一个过滤器,让用户只能看到自己的资源。
    filters = {"owner": ctx.user.identity}
    metadata = value.setdefault("metadata", {})
    metadata.update(filters)

    # 只让用户看到自己的资源
    return filters
该处理器接收两个参数:
  1. ctx (AuthContext):包含当前 user 的信息、用户的 permissionsresource(“threads”、“crons”、“assistants”)以及正在执行的 action(“create”、“read”、“update”、“delete”、“search”、“create_run”)。
  2. value (dict):正在创建或访问的数据。该字典的内容取决于正在访问的资源和操作。有关如何获得更精细的访问控制,请参阅下面的添加作用域授权处理器
请注意,这个简单的处理器做了两件事:
  1. 将用户ID添加到资源的元数据中。
  2. 返回一个元数据过滤器,使用户只能看到他们拥有的资源。

2. 测试私密对话

测试你的授权。如果设置正确,你将看到所有 ✅ 消息。请确保你的开发服务器正在运行(运行 langgraph dev):
from langgraph_sdk import get_client

# 为两个用户创建客户端
alice = get_client(
    url="http://localhost:2024",
    headers={"Authorization": "Bearer user1-token"}
)

bob = get_client(
    url="http://localhost:2024",
    headers={"Authorization": "Bearer user2-token"}
)

# Alice 创建一个助手
alice_assistant = await alice.assistants.create()
print(f"✅ Alice 创建了助手: {alice_assistant['assistant_id']}")

# Alice 创建一个线程并聊天
alice_thread = await alice.threads.create()
print(f"✅ Alice 创建了线程: {alice_thread['thread_id']}")

await alice.runs.create(
    thread_id=alice_thread["thread_id"],
    assistant_id="agent",
    input={"messages": [{"role": "user", "content": "你好,这是 Alice 的私密聊天"}]}
)

# Bob 尝试访问 Alice 的线程
try:
    await bob.threads.get(alice_thread["thread_id"])
    print("❌ Bob 不应该看到 Alice 的线程!")
except Exception as e:
    print("✅ Bob 被正确拒绝访问:", e)

# Bob 创建自己的线程
bob_thread = await bob.threads.create()
await bob.runs.create(
    thread_id=bob_thread["thread_id"],
    assistant_id="agent",
    input={"messages": [{"role": "user", "content": "你好,这是 Bob 的私密聊天"}]}
)
print(f"✅ Bob 创建了自己的线程: {bob_thread['thread_id']}")

# 列出线程 - 每个用户只能看到自己的
alice_threads = await alice.threads.search()
bob_threads = await bob.threads.search()
print(f"✅ Alice 看到 {len(alice_threads)} 个线程")
print(f"✅ Bob 看到 {len(bob_threads)} 个线程")
输出:
 Alice 创建了助手: fc50fb08-78da-45a9-93cc-1d3928a3fc37
 Alice 创建了线程: 533179b7-05bc-4d48-b47a-a83cbdb5781d
 Bob 被正确拒绝访问: Client error '404 Not Found' for url 'http://localhost:2024/threads/533179b7-05bc-4d48-b47a-a83cbdb5781d'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
 Bob 创建了自己的线程: 437c36ed-dd45-4a1e-b484-28ba6eca8819
 Alice 看到 1 个线程
 Bob 看到 1 个线程
这意味着:
  1. 每个用户可以创建并在自己的线程中聊天。
  2. 用户无法看到彼此的线程。
  3. 列出线程时只显示自己的线程。

3. 添加作用域授权处理器

宽泛的 @auth.on 处理器匹配所有授权事件。这很简洁,但也意味着 value 字典的内容没有明确的作用域,并且相同的用户级访问控制被应用于每个资源。如果你想更精细地控制,也可以控制特定资源上的特定操作。 更新 src/security/auth.py,为特定资源类型添加处理器:
# 保留我们之前的处理器...

from langgraph_sdk import Auth

@auth.on.threads.create
async def on_thread_create(
    ctx: Auth.types.AuthContext,
    value: Auth.types.on.threads.create.value,
):
    """创建线程时添加所有者。

    此处理器在创建新线程时运行,做两件事:
    1. 设置正在创建的线程的元数据以跟踪所有权。
    2. 返回一个过滤器,确保只有创建者可以访问它。
    """
    # 示例 value:
    #  {'thread_id': UUID('99b045bc-b90b-41a8-b882-dabc541cf740'), 'metadata': {}, 'if_exists': 'raise'}

    # 将所有者元数据添加到正在创建的线程中。
    # 此元数据随线程存储并持久化。
    metadata = value.setdefault("metadata", {})
    metadata["owner"] = ctx.user.identity


    # 返回过滤器以限制只有创建者可以访问。
    return {"owner": ctx.user.identity}

@auth.on.threads.read
async def on_thread_read(
    ctx: Auth.types.AuthContext,
    value: Auth.types.on.threads.read.value,
):
    """只让用户读取自己的线程。

    此处理器在读取操作时运行。我们不需要设置
    元数据,因为线程已经存在 - 我们只需要
    返回一个过滤器以确保用户只能看到自己的线程。
    """
    return {"owner": ctx.user.identity}

@auth.on.assistants
async def on_assistants(
    ctx: Auth.types.AuthContext,
    value: Auth.types.on.assistants.value,
):
    # 为了说明目的,我们将拒绝所有
    # 涉及助手资源的请求。
    # 示例 value:
    # {
    #     'assistant_id': UUID('63ba56c3-b074-4212-96e2-cc333bbc4eb4'),
    #     'graph_id': 'agent',
    #     'config': {},
    #     'metadata': {},
    #     'name': 'Untitled'
    # }
    raise Auth.exceptions.HTTPException(
        status_code=403,
        detail="User lacks the required permissions.",
    )

# 假设你按 (user_id, resource_type, resource_id) 组织存储中的信息。
@auth.on.store()
async def authorize_store(ctx: Auth.types.AuthContext, value: dict):
    # 每个存储项的 "namespace" 字段是一个元组,你可以将其视为项目的目录。
    namespace: tuple = value["namespace"]
    assert namespace[0] == ctx.user.identity, "Not authorized"
请注意,现在不是只有一个全局处理器,而是有针对以下操作的特定处理器:
  1. 创建线程
  2. 读取线程
  3. 访问助手
前三个匹配每个资源上的特定操作(参见资源操作),而最后一个(@auth.on.assistants)匹配 assistants 资源上的任何操作。对于每个请求,LangGraph 将运行与正在访问的资源和操作最匹配的处理器。这意味着上述四个处理器将运行,而不是宽泛作用域的 “@auth.on” 处理器。 尝试将以下测试代码添加到你的测试文件中:
# ... 与之前相同
# 尝试创建一个助手。这应该失败。
try:
    await alice.assistants.create("agent")
    print("❌ Alice 不应该能够创建助手!")
except Exception as e:
    print("✅ Alice 被正确拒绝访问:", e)

# 尝试搜索助手。这也应该失败。
try:
    await alice.assistants.search()
    print("❌ Alice 不应该能够搜索助手!")
except Exception as e:
    print("✅ Alice 被正确拒绝访问助手搜索:", e)

# Alice 仍然可以创建线程
alice_thread = await alice.threads.create()
print(f"✅ Alice 创建了线程: {alice_thread['thread_id']}")
输出:
 Alice 创建了线程: dcea5cd8-eb70-4a01-a4b6-643b14e8f754
 Bob 被正确拒绝访问: Client error '404 Not Found' for url 'http://localhost:2024/threads/dcea5cd8-eb70-4a01-a4b6-643b14e8f754'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
 Bob 创建了自己的线程: 400f8d41-e946-429f-8f93-4fe395bc3eed
 Alice 看到 1 个线程
 Bob 看到 1 个线程
 Alice 被正确拒绝访问:
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
 Alice 被正确拒绝访问助手搜索:
恭喜!你已经构建了一个聊天机器人,其中每个用户都有自己的私密对话。虽然这个系统使用简单的基于令牌的认证,但这些授权模式将与实现任何真实的认证系统一起工作。在下一个教程中,你将使用 OAuth2 替换你的测试用户为真实的用户账户。

后续步骤

现在你可以控制对资源的访问,你可能想要:
  1. 继续学习连接认证提供者以添加真实的用户账户。
  2. 阅读更多关于授权模式的内容。
  3. 查看 API 参考 以获取本教程中使用的接口和方法的详细信息。