跳到内容

使对话私密化(第 ⅔ 部分)

这是我们的身份验证系列教程的第 2 部分

  1. 基本身份验证 - 控制谁可以访问您的机器人
  2. 资源授权(您在此处) - 让用户拥有私密对话
  3. 生产环境身份验证 - 添加真实用户帐户并使用 OAuth2 进行验证

在本教程中,我们将扩展我们的聊天机器人,为每个用户提供他们自己的私密对话。我们将添加资源级访问控制,以便用户只能看到他们自己的线程。

Authorization handlers

占位符令牌

正如我们在第 1 部分中所做的那样,在本节中,我们将使用硬编码令牌进行演示。在掌握基础知识后,我们将在第 3 部分介绍“生产就绪”的身份验证方案。

理解资源授权

在上一个教程中,我们控制了谁可以访问我们的机器人。但是现在,任何通过身份验证的用户都可以看到其他人的对话!让我们通过添加资源授权来解决这个问题。

首先,请确保您已完成基本身份验证教程,并且您的安全机器人可以无错误运行

cd custom-auth
pip install -e .
langgraph dev --no-browser

添加资源授权

回想一下,在上次教程中,Auth 对象允许我们注册一个身份验证函数,LangGraph 平台使用该函数来验证传入请求中的承载令牌。现在我们将使用它来注册一个授权处理程序。

授权处理程序是在身份验证成功运行的函数。这些处理程序可以向资源添加元数据(例如,谁拥有它们)并过滤每个用户可以看到的内容。

让我们更新我们的 src/security/auth.py 并添加一个在每个请求上运行的授权处理程序

src/security/auth.py
from langgraph_sdk import Auth

# Keep our test users from the previous tutorial
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:
    """Our authentication handler from the previous tutorial."""
    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,  # Contains info about the current user
    value: dict,  # The resource being created/accessed
):
    """Make resources private to their creator."""
    # Examples:
    # 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': ... Your user object...
    #                 'langgraph_auth_user_id': 'user1'
    #             }
    #         },
    #         'stream_mode': ['values'],
    #         'interrupt_before': None,
    #         'interrupt_after': None,
    #         'webhook': None,
    #         'feedback_keys': None,
    #         'temporary': False,
    #         'subgraphs': False
    #     }
    # }

    # Do 2 things:
    # 1. Add the user's ID to the resource's metadata. Each LangGraph resource has a `metadata` dict that persists with the resource.
    # this metadata is useful for filtering in read and update operations
    # 2. Return a filter that lets users only see their own resources
    filters = {"owner": ctx.user.identity}
    metadata = value.setdefault("metadata", {})
    metadata.update(filters)

    # Only let users see their own resources
    return filters

处理程序接收两个参数

  1. ctx (AuthContext): 包含关于当前 user、用户的 permissionsresource(“threads”、“crons”、“assistants”)和正在执行的 action(“create”、“read”、“update”、“delete”、“search”、“create_run”)的信息
  2. value (dict): 正在创建或访问的数据。此字典的内容取决于正在访问的资源和操作。有关如何获得更严格作用域的访问控制的信息,请参阅下面的添加作用域授权处理程序

请注意,我们的简单处理程序执行两件事

  1. 将用户的 ID 添加到资源的元数据。
  2. 返回元数据过滤器,以便用户只能看到他们拥有的资源。

测试私密对话

让我们测试我们的授权。如果我们正确设置了所有内容,我们应该期望看到所有 ✅ 消息。请确保您的开发服务器正在运行(运行 langgraph dev

from langgraph_sdk import get_client

# Create clients for both users
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 creates an assistant
alice_assistant = await alice.assistants.create()
print(f"✅ Alice created assistant: {alice_assistant['assistant_id']}")

# Alice creates a thread and chats
alice_thread = await alice.threads.create()
print(f"✅ Alice created thread: {alice_thread['thread_id']}")

await alice.runs.create(
    thread_id=alice_thread["thread_id"],
    assistant_id="agent",
    input={"messages": [{"role": "user", "content": "Hi, this is Alice's private chat"}]}
)

# Bob tries to access Alice's thread
try:
    await bob.threads.get(alice_thread["thread_id"])
    print("❌ Bob shouldn't see Alice's thread!")
except Exception as e:
    print("✅ Bob correctly denied access:", e)

# Bob creates his own thread
bob_thread = await bob.threads.create()
await bob.runs.create(
    thread_id=bob_thread["thread_id"],
    assistant_id="agent",
    input={"messages": [{"role": "user", "content": "Hi, this is Bob's private chat"}]}
)
print(f"✅ Bob created his own thread: {bob_thread['thread_id']}")

# List threads - each user only sees their own
alice_threads = await alice.threads.search()
bob_threads = await bob.threads.search()
print(f"✅ Alice sees {len(alice_threads)} thread")
print(f"✅ Bob sees {len(bob_threads)} thread")

运行测试代码,您应该看到如下输出

 Alice created assistant: fc50fb08-78da-45a9-93cc-1d3928a3fc37
 Alice created thread: 533179b7-05bc-4d48-b47a-a83cbdb5781d
 Bob correctly denied access: Client error '404 Not Found' for url 'http://localhost:2024/threads/533179b7-05bc-4d48-b47a-a83cbdb5781d'
For more information check: https://mdn.org.cn/en-US/docs/Web/HTTP/Status/404
 Bob created his own thread: 437c36ed-dd45-4a1e-b484-28ba6eca8819
 Alice sees 1 thread
 Bob sees 1 thread

这意味着

  1. 每个用户都可以在他们自己的线程中创建和聊天
  2. 用户看不到彼此的线程
  3. 列出线程仅显示您自己的线程

添加作用域授权处理程序

广泛的 @auth.on 处理程序匹配所有授权事件。这很简洁,但这意味着 value 字典的内容作用域不明确,并且我们将相同的用户级访问控制应用于每个资源。如果我们想要更细粒度的控制,我们还可以控制资源的特定操作。

更新 src/security/auth.py 以添加特定资源类型的处理程序

# Keep our previous handlers...

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,
):
    """Add owner when creating threads.

    This handler runs when creating new threads and does two things:
    1. Sets metadata on the thread being created to track ownership
    2. Returns a filter that ensures only the creator can access it
    """
    # Example value:
    #  {'thread_id': UUID('99b045bc-b90b-41a8-b882-dabc541cf740'), 'metadata': {}, 'if_exists': 'raise'}

    # Add owner metadata to the thread being created
    # This metadata is stored with the thread and persists
    metadata = value.setdefault("metadata", {})
    metadata["owner"] = ctx.user.identity


    # Return filter to restrict access to just the creator
    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,
):
    """Only let users read their own threads.

    This handler runs on read operations. We don't need to set
    metadata since the thread already exists - we just need to
    return a filter to ensure users can only see their own threads.
    """
    return {"owner": ctx.user.identity}

@auth.on.assistants
async def on_assistants(
    ctx: Auth.types.AuthContext,
    value: Auth.types.on.assistants.value,
):
    # For illustration purposes, we will deny all requests
    # that touch the assistants resource
    # Example 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.",
    )

# Assumes you organize information in store like (user_id, resource_type, resource_id)
@auth.on.store()
async def authorize_store(ctx: Auth.types.AuthContext, value: dict):
    # The "namespace" field for each store item is a tuple you can think of as the directory of an item.
    namespace: tuple = value["namespace"]
    assert namespace[0] == ctx.user.identity, "Not authorized"

请注意,现在我们没有一个全局处理程序,而是有针对特定处理程序的

  1. 创建线程
  2. 读取线程
  3. 访问助手

前三个匹配每个资源上的特定操作(请参阅资源操作),而最后一个 (@auth.on.assistants) 匹配 assistants 资源上的任何操作。对于每个请求,LangGraph 将运行与正在访问的资源和操作最匹配的特定处理程序。这意味着上面的四个处理程序将运行,而不是作用域广泛的“@auth.on”处理程序。

尝试将以下测试代码添加到您的测试文件中

# ... Same as before
# Try creating an assistant. This should fail
try:
    await alice.assistants.create("agent")
    print("❌ Alice shouldn't be able to create assistants!")
except Exception as e:
    print("✅ Alice correctly denied access:", e)

# Try searching for assistants. This also should fail
try:
    await alice.assistants.search()
    print("❌ Alice shouldn't be able to search assistants!")
except Exception as e:
    print("✅ Alice correctly denied access to searching assistants:", e)

# Alice can still create threads
alice_thread = await alice.threads.create()
print(f"✅ Alice created thread: {alice_thread['thread_id']}")

然后再次运行测试代码

 Alice created thread: dcea5cd8-eb70-4a01-a4b6-643b14e8f754
 Bob correctly denied access: Client error '404 Not Found' for url 'http://localhost:2024/threads/dcea5cd8-eb70-4a01-a4b6-643b14e8f754'
For more information check: https://mdn.org.cn/en-US/docs/Web/HTTP/Status/404
 Bob created his own thread: 400f8d41-e946-429f-8f93-4fe395bc3eed
 Alice sees 1 thread
 Bob sees 1 thread
 Alice correctly denied access:
For more information check: https://mdn.org.cn/en-US/docs/Web/HTTP/Status/500
 Alice correctly denied access to searching assistants:

恭喜!您已经构建了一个聊天机器人,其中每个用户都有自己的私密对话。虽然此系统使用基于令牌的简单身份验证,但我们学到的授权模式将适用于实现任何真实的身份验证系统。在下一个教程中,我们将使用 OAuth2 将我们的测试用户替换为真实用户帐户。

下一步是什么?

现在您可以控制对资源的访问,您可能想要

  1. 继续学习生产环境身份验证以添加真实用户帐户
  2. 阅读有关授权模式的更多信息
  3. 查看 API 参考,了解本教程中使用的接口和方法的详细信息

评论