Как добавить аутентификацию в Gradio-агента и не запутаться в чекпоинтах
Разработчик столкнулся с проблемой: когда AI-агентом на Gradio начали пользоваться коллеги, стало невозможно отследить, кто чем занимается — история, чекпоинты и трейсы смешались в кучу. Решение — добавить простую аутентификацию через текстовый файл с логинами/паролями и научить систему разделять данные по пользователям в Langfuse и LangGraph. Автор показывает код на Python: парсинг файла учёток, защиту от timing-атак через secrets.compare_digest и привязку thread_id к username.
Если вы делитесь своим AI-агентом с командой, вам рано или поздно понадобится разделять пользователей — иначе история диалогов и чекпоинты превратятся в свалку. Статья показывает минималистичное решение для небольших команд, не требующее переделки интерфейса или подключения сложных систем аутентификации.
Проблема: кто это вообще пользуется моим агентом?
Автор собрал локального MCP-агента на LangGraph с Gradio-интерфейсом, трассировкой и чекпоинтами (это были предыдущие части цикла на Хабре). Всё работало отлично, пока агентом пользовался он один. Но стоило дать доступ коллегам — начался хаос: история диалогов, чекпоинты и трейсы смешались, и стало непонятно, кто за что отвечает.
Попытка вычислять пользователей по IP провалилась: контейнер в Docker Desktop (связка Windows → WSL → Docker) упорно показывал один и тот же IP-адрес. Решение — добавить простую аутентификацию в Gradio.
Решение: текстовый файл вместо базы данных
Gradio не предлагает встроенной регистрации — нужно либо самому заводить пользователей, либо подключать внешнюю аутентификацию. Для пары коллег автор выбрал минимализм: текстовый файл auth_users.txt с парами логин:пароль (по одной на строку, комментарии через #).
Код парсит файл при старте, складирует учётки в словарь и возвращает функцию-проверку для Gradio:
def checker(username: str, password: str) -> bool:
stored = credentials.get(username)
return stored is not None and secrets.compare_digest(stored, password)
Зачем secrets.compare_digest? Защита от timing-атак: обычное == выходит из сравнения при первом несовпадении символов, и злоумышленник может подбирать пароль по времени ответа. compare_digest работает за константное время.
Интеграция с Gradio
Добавить аутентификацию в интерфейс — буквально две строчки:
self.demo.launch(
auth=get_gradio_auth(),
auth_message='<p>Введите выданный Вам логин и пароль</p>'
)
Разделение данных по пользователям
Теперь thread_id для LangGraph и Langfuse формируется из username и request_id:
def resolve_thread_id(username: str | None, request_id: str) -> str:
return f'{username or "anonymous"}:{request_id}'
Если аутентификация отключена, username будет None, и система fallback'ится на anonymous. Каждый пользователь получает свою изолированную историю, чекпоинты и трейсы в Langfuse — больше никакой каши.
Ключевые выводы
- Gradio не имеет встроенной регистрации — аутентификацию нужно делать самому или подключать извне
- Для малых команд (2–3 человека) текстовый файл с учётками — вполне рабочее решение вместо полноценной базы данных
- secrets.compare_digest защищает от timing-атак даже при хранении паролей в plaintext — минимальная безопасность лучше никакой
- Привязка thread_id к username решает проблему разделения истории и чекпоинтов между пользователями в LangGraph
- Fallback на "anonymous" при отключённой аутентификации позволяет коду работать в обоих режимах без дублирования логики
Автор: GoldenGekko · Источник: habr.com
Это редкий случай, когда статья на Хабре решает **бытовую** проблему разработчика, а не гонится за хайпом. Автор честно признаётся: у него нет задачи строить enterprise-систему с OAuth и ролевой моделью — ему нужно, чтобы три коллеги не путались в чекпоинтах. И он выбирает текстовый файл с паролями. Звучит дико? Возможно. Но для прототипа или внутреннего инструмента это **адекватный** выбор: не надо тащить PostgreSQL, Redis, Keycloak и прочий зоопарк ради троих человек.
Особенно приятно, что автор не забывает про базовую безопасность: `secrets.compare_digest` вместо наивного `==` — мелочь, но показывает культуру кода. Да, пароли в plaintext — это не круто, но если файл лежит внутри контейнера и доступен только вам, риски минимальны. Главное — не выкладывать его в публичный репозиторий (надеюсь, автор в курсе).
**А ВОТ ПРОТИВ:** Текстовый файл с паролями в открытом виде — это как оставить ключи под ковриком. Да, для троих человек в изолированной среде это не критично, но стоит кому-то скопировать код в продакшн или расшарить контейнер — и вы получите дыру. `secrets.compare_digest` защищает от timing-атак, но не от утечки самого файла. Почему бы сразу не использовать `bcrypt` или `argon2` для хеширования? Библиотека весит килобайты, а код меняется на три строчки. Да и OAuth через Google/GitHub для Gradio настраивается за полчаса — зачем изобретать велосипед?
Комментарии