Друзья, приветствую! Давно хотел рассказать вам о такой замечательной open-source технологии, как Keycloak на примере реального веб-приложения. Сегодня я расскажу вам простым и доступным языком о том, что это за технология и почему она используется даже в банковских приложениях.
Практическую часть я буду демонстрировать на связке Keycloak + FastAPI (Python), но если вы бэкенд-разработчик на другом языке программирования, то данная информация будет для вас такой же полезной, так как основные принципы работы с Keycloak универсальны.
О чем эта статья?
Сегодняшняя статья будет разбита на два основных блока:
Теоретическая часть – здесь я коротко и понятно расскажу вам про Keycloak и о принципах его работы.
Практическая часть – приступим к разработке нашего веб-приложения с интегрированной системой авторизации и аутентификации.
Веб-приложение, которое мы разработаем, будет представлять собой CRUD-приложение для заметок. Хотя внутренняя архитектура приложения может быть любой, наша главная цель – реализовать надежную систему авторизации с помощью Keycloak.
Ключевой момент нашего проекта: попадая на главную страницу приложения, пользователь, не выполнивший авторизацию, будет автоматически перенаправлен на форму входа/регистрации, которая полностью сгенерирована Keycloak “из коробки” и только после входа в систему под своим логином и паролем, пользователь будет попадать в систему собственных заметок.
Важно понимать: мы не будем создавать собственную форму регистрации, авторизации, логику входа по почте, отправку сообщений на email и многое другое, ведь это все уже есть в Keycloak - наша задача интегрировать эти готовые инструменты в свою систему.


План действий
В рамках статьи мы выполним следующие шаги:
Развернем Keycloak с внутренней базой данных (я покажу простой способ запуска сервиса Keycloak на платформе Amvera Cloud – процесс займет буквально несколько минут, а вы получите универсальную систему авторизации и аутентификации, которую можно использовать в различных проектах)
Подготовим базу данных для нашего проекта (не путать с базой данных пользователей Keycloak)
Разработаем API для управления заметками: добавление, просмотр, редактирование, удаление заметок и другие необходимые операции
Создадим фронтенд с использованием Jinja2, HTML, CSS и JavaScript
Развернем проект на Amvera Cloud (займет всего пару минут)
Когда вы познакомитесь с Keycloak, я уверен, что вы начнете активно использовать эту технологию в своих проектах.
Что такое Keycloak?
Keycloak – это бесплатное решение с открытым исходным кодом для управления идентификацией и доступом (Identity and Access Management, IAM). Основная задача Keycloak – обеспечить механизм единого входа (Single Sign-On, SSO), позволяющий пользователям аутентифицироваться один раз и получать доступ к различным приложениям без необходимости повторного ввода учетных данных.
Если простыми словами
Keycloak – это система, которая берет на себя всю сложную работу по:
Аутентификации (подтверждению личности пользователя)
Авторизации (управлению правами доступа)
Управлению сессиями пользователей
Защите API и веб-приложений
Благодаря этому разработчикам не приходится писать много кода для обеспечения безопасности своих приложений. Keycloak поддерживает вход через социальные сети, двухфакторную аутентификацию, интеграцию с корпоративными системами каталогов (LDAP/Active Directory) и множество других функций.
Ключевые преимущества Keycloak
Единая точка входа (SSO) – пользователи входят один раз и получают доступ ко всем связанным приложениям
Централизованное управление – все пользователи, роли и группы управляются через единую административную панель
Готовые страницы авторизации – формы входа, регистрации, восстановления пароля уже реализованы и настраиваются визуально
Поддержка стандартов – полная совместимость с OpenID Connect, OAuth 2.0 и SAML 2.0
Реалмы (Realms) – возможность создания изолированных областей для разных групп пользователей или проектов
Гибкость интеграции – официальные адаптеры для популярных фреймворков и возможность создания собственных
Низкий порог входа – простое внедрение даже для разработчиков без глубоких знаний в области безопасности
Кастомизация внешнего вида – возможность полностью настроить дизайн страниц аутентификации под ваш бренд (сегодня рассматривать не будем)
Архитектура Keycloak
Архитектура Keycloak включает два основных компонента:
Сервер Keycloak – центральный сервер аутентификации, который управляет пользователями и предоставляет функции безопасности
Адаптеры Keycloak – библиотеки или фреймворки, которые интегрируются в ваши приложения для взаимодействия с сервером Keycloak (в нашем случае речь про FastAPI)
Процесс аутентификации обычно выглядит так:
Запрос защищенного ресурса — Пользователь пытается получить доступ к защищенному ресурсу в вашем приложении.
Перенаправление на Keycloak — Адаптер Keycloak в вашем приложении обнаруживает, что пользователь не авторизован, и перенаправляет его на сервер Keycloak для прохождения аутентификации.
Ввод учетных данных — Пользователь попадает на страницу входа Keycloak, где вводит свои учетные данные (логин/пароль) или выбирает другой метод аутентификации (например, вход через Google или Facebook) / проходит регистрацию.
Создание авторизационного кода — После успешной проверки учетных данных Keycloak не сразу отправляет токены доступа. Вместо этого он генерирует временный авторизационный код и перенаправляет пользователя обратно в приложение с этим кодом.
Обмен кода на токены — Ваше приложение получает авторизационный код и отправляет запрос обратно к Keycloak для обмена этого кода на полноценные токены аутентификации. Этот шаг происходит “за кулисами” между серверами, без участия пользователя.
-
Получение токенов — В ответ на корректный авторизационный код Keycloak предоставляет вашему приложению три ключевых токена:
access_token — основной токен для доступа к защищенным ресурсам (обычно действует короткое время, например, 5-30 минут)
refresh_token — токен для получения нового access_token без повторной аутентификации пользователя (обычно действует дольше, например, несколько дней)
id_token — содержит информацию о пользователе (профиль)
Доступ к ресурсам — Теперь ваше приложение может использовать полученный access_token для проверки подлинности пользователя и предоставления доступа к защищенным ресурсам.
Таким образом, Keycloak помогает сделать приложения безопасными, упрощает управление пользователями и обеспечивает удобный и надежный механизм входа для пользователей.
Когда стоит использовать Keycloak?
Keycloak особенно полезен в следующих сценариях:
Вы разрабатываете несколько приложений, которые должны использовать общую базу пользователей. Keycloak позволит вам избежать дублирования кода аутентификации и обеспечит единую точку входа для всех ваших сервисов.
Вам нужна расширенная функциональность аутентификации - двухфакторная аутентификация, вход через социальные сети, восстановление пароля, верификация email и т.д.
У вас есть требования к безопасности, которые сложно реализовать самостоятельно. Keycloak соответствует современным стандартам безопасности и постоянно обновляется.
Вам необходимо централизованное управление пользователями - создание, блокировка, установка ролей и прав доступа через удобный административный интерфейс.
Вы разрабатываете корпоративное приложение, которое должно интегрироваться с существующими системами аутентификации (LDAP, Active Directory).
Основные компоненты Keycloak
Перед тем как мы перейдем к практике, давайте кратко рассмотрим основные компоненты и термины, которые нам понадобятся:
Realm (Реалм) — изолированная область, в которой определяются пользователи, клиенты, роли и группы. Можно создавать несколько реалмов для разных проектов или окружений.
Client (Клиент) — приложение, которое может запрашивать аутентификацию у Keycloak. В нашем случае клиентом будет наше FastAPI-приложение.
User (Пользователь) — субъект, который может аутентифицироваться в Keycloak.
Role (Роль) — набор прав, которые могут быть назначены пользователям. Роли могут быть привязаны к конкретному клиенту или быть глобальными для реалма.
Group (Группа) — набор пользователей, которым можно одновременно назначить определенные роли.
Protocol (Протокол) — Keycloak поддерживает несколько протоколов аутентификации, включая OpenID Connect (расширение OAuth 2.0) и SAML 2.0. В нашем проекте мы будем использовать OpenID Connect.
Теперь, когда у нас есть базовое понимание Keycloak и его возможностей, давайте перейдем к практической части нашего руководства.
Поднимаем Keycloak с внутренней базой данных
Если вы следите за моим творчеством, то знаете, что я, в основном, ориентируюсь в статьях на новичков. Поэтому, далее я расскажу о самом простейшем способе поднять данную систему с минимальным количеством телодвижений.
В развертывании, как базы данных под Keycloak, так и самой системы Keycloak – нам поможет сервис Amvera Cloud.
Важно о базе данных
База данных должна быть изолирована. То есть, не желательно использовать одну базу данных и для проекта, и для Keycloak. Для Keycloak подходят базы данных MySQL и PostgreSQL. Мы поднимем PostgreSQL.
Поднять можно как собственную базу данных (например, на VPS сервере), либо на сервисе Amvera. Выберем последнее, так как это проще и не требует специальных навыков.
Поднимаем PostgreSQL на Amvera Cloud
Регистрируемся на сайте Amvera, если ещё не было регистрации, и входим
На главной странице выбираем «PostgreSQL»
Нажимаем на кнопку «Создать базу данных»
Задаем «Название проекта», выбираем подходящий тариф (не ниже «Начальный»)
Задаем параметры СУБД PostgreSQL:
Нажимаем кнопку «Завершить» и дожидаемся перехода проекта в статус «PostgreSQL запущен».

Этих действий будет достаточно для того, чтобы использовать эту базу данных для Keycloak. Сохраните все данные о подключении - они нам понадобятся на следующем этапе.
Поднимаем сам Keycloak
Для развертывания Keycloak вам потребуется выполнить следующие шаги:
-
Создаем проект:
Тип: Преднастроенное приложение из маркетплейса
Параметры сервиса: Авторизация
Тип сервиса: Keycloak
Тариф: не ниже «Начальный» для стабильной работы

-
На этапе «Конфигурация» настраиваем переменные окружения (envvars):
KC_BOOTSTRAP_ADMIN_USERNAME
— имя (login) администратораKC_BOOTSTRAP_ADMIN_PASSWORD
— временный пароль администратораKC_DB
— тип базы данных. Указываемpostgres
.KC_DB_URL_HOST
— host базы данных (тут задается ссылка для чтения/записи)KC_DB_URL_PORT
— порт базы данных (по умолчанию 5432 для PostgreSQL, если поднимали на Amvera)KC_DB_URL_DATABASE
— имя базы данныхKC_DB_USERNAME
— имя пользователя базы данныхKC_DB_PASSWORD
— пароль базы данных
-
В разделе «Настройки» приложения:
Активируем бесплатное доменное имя или добавляем своё
-
Добавляем переменную:
KC_HOSTNAME
— ваше доменное имя

Перезапускаем проект. После перезапуска, если все прошло корректно, переходим по выделенному доменному имени и должны увидеть такое окно:

Знакомство с админкой Keycloak и первый вход
Этой теме легко можно было бы посвятить отдельную статью, но, чтобы не тратить ваше время, проведу короткий, но насыщенный экскурс.
Вход в административную панель
Для входа в админку открываем в браузере доменное имя, указанное в переменной KC_HOSTNAME
. Вас сразу перебросит на страницу авторизации. Вводим логин и пароль — если данные корректны, встречаемся с административной панелью Keycloak.

Так как мы ранее задавали временные учетные данные, Keycloak предупредит вас об этом. Чтобы избавиться от надоедливого предупреждения, создадим постоянного пользователя и удалим временную учетку. Важный момент: сначала создаём нового пользователя, а уже потом удаляем старого, иначе останетесь без доступа.
Создание постоянного администратора
Переходим на вкладку Users, затем:
Нажимаем Add User.

Вводим имя пользователя (это обязательное поле). Рекомендуется также указать реальный email.
Нажимаем Create.
После создания возвращаемся в карточку пользователя. На вкладке Credentials задаём пароль, не забывая включить флаг Temporary = OFF.
Далее переходим на вкладку Role mapping и назначаем все роли администратора (
realm-admin
,admin
, и т.д.).

После этого можно смело удалить временную учетную запись. Система выкинет вас на страницу входа — вводим новые данные, подтверждаем пароль, и больше никакие предупреждения нас не беспокоят.
Создание Realm под проект
Как уже упоминалось, Realm в Keycloak — это изолированное пространство, внутри которого живёт свой набор пользователей, настроек, клиентов и прочего. Можно условно считать его отдельной базой данных и мини-админкой под конкретный проект.
Обычно в дефолтном realm master
создаётся основной админ, а далее под каждый проект создаются собственные реалмы. Так и поступим:
В верхнем левом углу кликаем по названию текущего реалма (
master
).Выбираем Create realm.

Задаём имя (например
my-project
) и нажимаем Create.Проваливаемся в только что созданное пространство.
Создание клиента
Важно понимать разницу между клиентами и пользователями. В Keycloak клиент (client) — это приложение, которое будет использовать аутентификацию через Keycloak. А пользователи (users) — это конечные учетные записи в базе.
Чтобы создать клиента:
Открываем вкладку Clients.
Нажимаем Create client.

В поле Client type оставляем
OpenID Connect
(про SAML поговорим в отдельной статье).Вводим Client ID — имя клиента на латинице, например
fastapi-client
. Нажимаем Next.Включаем флаг Client authentication, остальное можно оставить по умолчанию. Жмём Next.
Настройки на следующем экране пока пропускаем — нажимаем Save.
Клиент создан.
Подключение почты (опционально, но полезно)
Если вы хотите, чтобы пользователи получали письма с подтверждением почты, необходимо настроить SMTP-сервер.
Для этого идём в Realm settings → Email.

Ключевой момент: у Keycloak нет встроенного почтового сервиса. Придётся подключить сторонний SMTP. Можно использовать бесплатные варианты (например, Yandex или Mail.ru) или арендовать почтовик под собственный домен (например, через reg.ru).
Я использовал Yandex Почту. Там нужно:
Открыть доступ для сторонних приложений.
Создать специальный пароль для внешних приложений.
После этого заполняем поля:
From — адрес, с которого будут отправляться письма (например
yakvenalex@yandex.ru
).From display name — имя, отображаемое у получателя (например
Mimo Support
).Host —
smtp.yandex.ru
Port —
587
Enable StartTLS — включаем
Enable Authentication — включаем
Далее вводим логин и пароль от почты (в моем случае это yakvenalex@yandex.ru
и ранее созданный пароль приложения).
Нажимаем Test — если всё верно, получите тестовое письмо.

Чтобы активировать email-аутентификацию:
Переходим на вкладку Login.
Активируем все чекбоксы, связанные с подтверждением и восстановлением пароля.

Готово!
Что дальше?
К админке мы ещё вернёмся, когда будем подключать домен нашего веб-приложения. А пока — переходим к коду!
Подготавливаем проект
Теперь настало время обсудить стек технологий, который мы будем использовать в нашем проекте.
Технологии, которые мы будем использовать
Основной стек на Python:
FastAPI — на мой взгляд, лучший фреймворк для создания современного backend’а.
SQLAlchemy — ORM для взаимодействия с базой данных.
Alembic — инструмент для управления миграциями.
Httpx — HTTP-клиент, с помощью которого FastAPI будет взаимодействовать с Keycloak.
Aiosqlite — асинхронный движок для SQLite.
Jinja2 — шаблонизатор для рендера HTML-страниц (идеально под простые CRUD-интерфейсы).
По фронтенду:
Я не использовал никаких frontend-фреймворков. Всё на чистом HTML + CSS + JS. Чтобы меньше возиться со стилями и JavaScript — подключил Bootstrap 5.
Настройка окружения
Создаём виртуальное окружение и в корне проекта добавляем два файла:
.env
— переменные окруженияrequirements.txt
— зависимости проекта
requirements.txt
fastapi==0.115.12
uvicorn==0.34.2
httpx==0.28.1
loguru==0.7.3
pydantic==2.11.4
pydantic-settings==2.9.1
alembic==1.13.3
SQLAlchemy==2.0.35
aiosqlite==0.20.0
Jinja2==3.1.6
Устанавливаем зависимости:
pip install -r requirements.txt
Переменные окружения
Создаём .env
и добавляем туда:
BASE_URL="ссылка на домен бэкенда"
KEYCLOAK_BASE_URL="ссылка на домен Keycloak"
REALM="имя realm"
CLIENT_ID="имя клиента"
CLIENT_SECRET="секрет клиента"
Пример:
BASE_URL="https://fastapikeycloack-yakvenalex.amvera.io"
KEYCLOAK_BASE_URL="https://authservice-yakvenalex.amvera.io"
REALM="fastapi-realm"
CLIENT_ID="fastapi-client"
CLIENT_SECRET="my_super_secret"
Где взять CLIENT_SECRET и как подключить бэкенд?
Если вы работаете локально и хотите быстро всё протестировать, можно использовать временное доменное имя от Ngrok, Tuna или любого аналогичного сервиса. Главное — убедитесь, что порт совпадает с портом запуска вашего FastAPI-приложения.
Получение секрета клиента:
Заходим в админку Keycloak.
Переходим в нужный client.
Открываем вкладку Credentials.
Там уже будет сгенерированный
secret
, при желании можно сгенерировать новый.

Настройка клиента:
Пока вы в админке, откройте вкладку Settings у клиента и добавьте туда URL-адреса вашего бэкенда.
На что обратить внимание:
Web Origins — я указал
*
, чтобы не блокировалась авторизация на любом домене. Но в боевых проектах обязательно указывайте конкретные домены.-
Valid redirect URIs — одна из важнейших настроек. Именно сюда Keycloak отправит код авторизации после успешного входа. В нашем случае это, например:
https://fastapikeycloack-yakvenalex.amvera.io/api/login/callback
Остальные поля достаточно очевидны, и мы к ним вернёмся при необходимости.

Конфигурация проекта
Создаём папку app/
и внутри неё — файл config.py
. Заполняем его следующим образом:
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
KEYCLOAK_BASE_URL: str
BASE_URL: str
REALM: str
CLIENT_ID: str
CLIENT_SECRET: str
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
@property
def database_url(self) -> str:
return f"sqlite+aiosqlite:///{self.BASE_DIR}/data/db.sqlite3"
@property
def token_url(self) -> str:
return f"{self.KEYCLOAK_BASE_URL}/realms/{self.REALM}/protocol/openid-connect/token"
@property
def auth_url(self) -> str:
return (
f"{self.KEYCLOAK_BASE_URL}/realms/{self.REALM}/protocol/openid-connect/auth"
)
@property
def logout_url(self) -> str:
return f"{self.KEYCLOAK_BASE_URL}/realms/{self.REALM}/protocol/openid-connect/logout"
@property
def userinfo_url(self) -> str:
return f"{self.KEYCLOAK_BASE_URL}/realms/{self.REALM}/protocol/openid-connect/userinfo"
@property
def redirect_uri(self) -> str:
return f"{self.BASE_URL}/api/login/callback"
model_config = SettingsConfigDict(env_file=f"{BASE_DIR}/.env")
settings = Settings()
Комментарии к конфигурации
Используем
pydantic-settings
, чтобы удобно тянуть переменные из.env
.BASE_DIR
рассчитывается автоматически, чтобы всё работало вне зависимости от того, откуда вы запускаете проект.Отдельными свойствами выделены ключевые URL-адреса, с которыми мы будем работать при взаимодействии с Keycloak — токены, авторизация, информация о пользователе, logout и т.д.
redirect_uri
— это endpoint, на который Keycloak будет отправлять код после логина.
На этом подготовительный этап завершён — всё готово, чтобы приступить к логике интеграции с Keycloak!
Настраиваем Keycloak в FastAPI-приложении
Переходим к самому интересному — интеграции FastAPI с Keycloak. Чтобы всё было аккуратно и удобно, создаём структуру:
app/
├── services/
│ ├── keycloak_client.py
│ └── auth_dep.py
Общая логика работы с Keycloak
Прежде чем перейти к коду, объясню, что мы вообще собираемся делать.
Когда пользователь проходит авторизацию через Keycloak и успешно входит, мы получаем набор токенов. Самое логичное решение — сохранить эти токены в cookie-сессии. А дальше при каждом запросе:
Проверяем, есть ли
access_token
в cookie.Если токен есть — проверяем его на валидность, запрашивая
userinfo
у Keycloak.Если токен жив и валиден — пускаем пользователя.
Если токена нет или он просрочен — редиректим на страницу входа.
Вся работа с Keycloak будет происходить через httpx
.
keycloak_client.py — наш клиент для Keycloak
import httpx
from fastapi import HTTPException
from app.config import settings
class KeycloakClient:
def __init__(self, client: httpx.AsyncClient | None = None):
self.client = client or httpx.AsyncClient()
async def get_tokens(self, code: str) -> dict:
"""Обмен authorization code на токены"""
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": settings.redirect_uri,
"client_id": settings.CLIENT_ID,
"client_secret": settings.CLIENT_SECRET,
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
try:
response = await self.client.post(
settings.token_url, data=data, headers=headers
)
if response.status_code != 200:
raise HTTPException(
status_code=401, detail=f"Token request failed: {response.text}"
)
return response.json()
except httpx.RequestError as e:
raise HTTPException(
status_code=500, detail=f"Token exchange failed: {str(e)}"
)
async def get_user_info(self, token: str) -> dict:
"""Получить информацию о пользователе по access_token"""
headers = {"Authorization": f"Bearer {token}"}
try:
response = await self.client.get(settings.userinfo_url, headers=headers)
if response.status_code != 200:
raise HTTPException(
status_code=401, detail=f"Invalid access token: {response.text}"
)
return response.json()
except httpx.RequestError as e:
raise HTTPException(
status_code=500, detail=f"Keycloak request error: {str(e)}"
)
Разбор:
init
: принимает внешнийhttpx.AsyncClient
, либо создаёт свой. Это нужно, чтобы переиспользовать подключение, не создавая нового при каждом запросе.get_tokens
: отправляемauthorization_code
, полученный от Keycloak, и получаем три токена:access_token
,refresh_token
,id_token
.get_user_info
: поaccess_token
получаем информацию о пользователе.
Эти методы — ядро, вокруг которого будет строиться логика авторизации.
Как связать пользователя из Keycloak с нашей базой данных?
Всё просто. У каждого пользователя в Keycloak есть уникальный идентификатор (UUID). При первом входе можно:
Проверить — есть ли такой пользователь в нашей базе.
Если нет — создать запись.
Если есть — использовать как есть.
Таким образом, get_user_info
помогает не только валидировать токен, но и привязать Keycloak-аккаунт к учётной записи в нашем приложении.
auth_dep.py — зависимости для FastAPI
from fastapi import Depends, HTTPException, Request
from app.services.keycloak_client import KeycloakClient
#Получаем KeycloakClient из app.state
def get_keycloak_client(request: Request) -> KeycloakClient:
return request.app.state.keycloak_client
#Получаем токен из cookie
async def get_token_from_cookie(request: Request) -> str | None:
return request.cookies.get("access_token")
#Получаем пользователя по токену
async def get_current_user(
token: str = Depends(get_token_from_cookie),
keycloak: KeycloakClient = Depends(get_keycloak_client),
) -> dict:
if not token:
raise HTTPException(status_code=401, detail="Unauthorized: No access token")
try:
user_info = await keycloak.get_user_info(token)
return user_info
except HTTPException:
raise HTTPException(status_code=401, detail="Invalid token")
Комментарии по коду:
get_keycloak_client
def get_keycloak_client(request: Request) -> KeycloakClient:
return request.app.state.keycloak_client
Это та самая «фишка» FastAPI, о которой я упоминал: app.state
. При запуске приложения мы создадим один экземпляр клиента и положим его туда. Это позволяет переиспользовать httpx.AsyncClient
между запросами — без потерь на постоянные подключения.
get_token_from_cookie
Тут всё просто — достаём access_token
из cookie. Если его нет — пользователь не авторизован.
get_current_user
Объединяем всё вместе:
достаём токен,
проверяем его через Keycloak,
возвращаем информацию о пользователе, если всё ок.
Если что-то не так — возвращаем 401-ю ошибку. Позже в роутере можем на эту ошибку отреагировать, например, редиректом на /login
.
Теперь необходимо реализовать логику внутренней базы данных.
Внутренняя база данных: SQLAlchemy и свои таблицы
Если при работе с Keycloak и PostgreSQL всё автоматически — Keycloak сам создаёт нужные таблицы и схемы, то с нашей внутренней базой данных так не выйдет. Нам придётся вручную описывать таблицы пользователей и заметок, а поможет нам в этом, конечно же, SQLAlchemy 2.
Так как статья и так уже стала довольно объёмной, я не буду повторять всё, что уже не раз показывал — например, структуру базового класса BaseDAO
, миграции с Alembic и прочие архитектурные моменты. За этим — добро пожаловать в мои предыдущие статьи или серию публикаций по SQLAlchemy и Alembic. Здесь — только суть.
Структура DAO
В папке app
создаём новую папку dao
и раскладываем файлы:
base.py
— базовый класс с универсальными методами для работы с таблицами.dao.py
— конкретные DAO-классы, удобно, чтобы выносить индивидуальную логику.database.py
— настройки подключения к базе.models.py
— ORM-модели.
Подключение к базе (database.py)
from datetime import datetime
from sqlalchemy import TIMESTAMP, func
from sqlalchemy.ext.asyncio import (
AsyncAttrs,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from app.config import settings
engine = create_async_engine(url=settings.database_url)
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
class Base(AsyncAttrs, DeclarativeBase):
__abstract__ = True
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
TIMESTAMP, server_default=func.now(), onupdate=func.now()
)
Обычная настройка движка и фабрики асинхронных сессий. Особенность — выносим created_at
и updated_at
в базовый класс, чтобы не дублировать их в каждой модели.
Модели (models.py)
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.dao.database import Base
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True)
email_verified: Mapped[bool]
name: Mapped[str]
preferred_username: Mapped[str]
given_name: Mapped[str]
family_name: Mapped[str]
notes: Mapped[list["Note"]] = relationship(back_populates="user")
class Note(Base):
__tablename__ = "notes"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
content: Mapped[str]
user_id: Mapped[str] = mapped_column(ForeignKey("users.id"))
user: Mapped["User"] = relationship(back_populates="notes")
У нас две таблицы: users
и notes
.
id
у пользователей — строка. Это упрощённый подход для SQLite, но в боевом PostgreSQL лучше использовать UUID.В
User
храним все ключевые поля, которые отдаёт Keycloak:email
, имя, фамилия и т. д.Устанавливаем
relationship
между пользователями и их заметками.
Зависимости под сессию (services/dao_dep.py)
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.dao.database import async_session_maker
async def get_session_with_commit() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def get_session_without_commit() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
Ничего особенного — стандартные зависимости под FastAPI для работы с сессией, с и без авто-коммита.
Базовый DAO (base.py)
class BaseDAO(Generic[T]):
model: Type[T] = None # type: ignore
def __init__(self, session: AsyncSession):
self._session = session
if self.model is None:
raise ValueError("Модель должна быть указана в дочернем классе")
async def find_one_or_none_by_id(self, data_id: int | str):
try:
query = select(self.model).filter_by(id=data_id)
result = await self._session.execute(query)
record = result.scalar_one_or_none()
log_message = f"Запись {self.model.__name__} с ID {data_id} {'найдена' if record else 'не найдена'}."
logger.info(log_message)
return record
except SQLAlchemyError as e:
logger.error(f"Ошибка при поиске записи с ID {data_id}: {e}")
raise
Это базовый класс. Тут реализован один метод find_one_or_none_by_id
, остальные — по аналогии. Напоминаю, что полный код проекта, в том числе и этого базового класса, вы найдете в моем телеграмм канале "Легкий путь в Python".
Дочерние DAO-классы (dao.py)
from app.dao.base import BaseDAO
from app.dao.models import Note, User
class UsersDAO(BaseDAO):
model = User
class NotesDAO(BaseDAO):
model = Note
Просто задаём модель и получаем всё поведение от базового класса. При инициализации передаём сессию — и можно работать.
Миграции
Миграции — через Alembic. Здесь подробно не расписываю, чтобы не растягивать статью. В моих других публикациях это показано пошагово. С подходом по миграции можно ознакомиться, например, в статье "Создание блога на FastAPI с нуля: JWT, Markdown и современный веб-дизайн".
На этом всё. Таблицы описаны, DAO готовы, зависимости для работы сессий подключены. Можно переходить к описанию API.
Реализуем API-методы входа и выхода из системы
В текущем проекте API-методов будет не так много, поэтому разберём их подробно и детально. Фронтенд-часть рассмотрим более кратко.
Для удобства организации структуры создаём в папке app
папку api
, и помещаем внутрь два файла: route.py
(для описания API-методов) и schemas.py
(для описания Pydantic-моделей, необходимых для валидации данных).
Начнём с файла schemas.py
:
from pydantic import BaseModel
class SUserId(BaseModel):
user_id: str
class AddUser(BaseModel):
id: str
email: str
email_verified: bool
name: str
preferred_username: str
given_name: str
family_name: str
class AddNote(BaseModel):
title: str
content: str
class AddNoteWithUserId(AddNote, SUserId):
pass
Классический подход в FastAPI. Далее вы увидите, как эти модели используются в эндпоинтах.
Приступим к файлу app/api/
router.py
. Сначала выполним необходимые импорты и инициализируем роутер:
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from loguru import logger
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.schemas import AddNote, AddNoteWithUserId, AddUser, SUserId
from app.config import settings
from app.dao.dao import NotesDAO, UsersDAO
from app.services.auth_dep import get_current_user, get_keycloak_client
from app.services.dao_dep import (
get_session_with_commit,
get_session_without_commit,
)
from app.services.keycloak_client import KeycloakClient
router = APIRouter(prefix="/api", tags=["API"])
Для логирования будем использовать loguru
. Остальные импорты должны быть понятны, если вы внимательно читали предыдущий материал. Тем не менее, всё необходимое будем разбирать по ходу.
Начнём с самого важного эндпоинта — того, который связывает наше приложение с Keycloak:
@router.get("/login/callback", include_in_schema=False)
async def login_callback(
code: str | None = None,
error: str | None = None,
error_description: str | None = None,
session: AsyncSession = Depends(get_session_with_commit),
keycloak: KeycloakClient = Depends(get_keycloak_client),
) -> RedirectResponse:
"""
Обрабатывает callback после авторизации в Keycloak.
Получает токен, информацию о пользователе, сохраняет пользователя в БД (если нужно)
и устанавливает cookie с токенами. Обрабатывает ошибки от Keycloak.
"""
if error:
logger.error(f"Keycloak error: {error}, description: {error_description}")
raise HTTPException(status_code=401, detail="Authorization code is required")
if not code:
raise HTTPException(status_code=401, detail="Authorization code is required")
try:
# Получение токенов от Keycloak
token_data = await keycloak.get_tokens(code)
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
id_token = token_data.get("id_token")
if not access_token:
raise HTTPException(status_code=401, detail="Токен доступа не найден")
if not refresh_token:
raise HTTPException(status_code=401, detail="Refresh token не найден")
if not id_token:
raise HTTPException(status_code=401, detail="ID token не найден")
# Получение информации о пользователе
user_info = await keycloak.get_user_info(access_token)
user_id = user_info.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="ID пользователя не найден")
# Проверка существования пользователя, создание нового при необходимости
users_dao = UsersDAO(session)
user = await users_dao.find_one_or_none_by_id(user_id)
if not user and isinstance(user_info, dict):
user_info["id"] = user_info.pop("sub")
await users_dao.add(AddUser(**user_info))
# Установка cookie с токенами и редирект
response = RedirectResponse(url="/protected")
response.set_cookie(
key="access_token",
value=access_token,
httponly=True,
secure=True,
samesite="lax",
path="/",
max_age=token_data.get("expires_in", 3600),
)
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="lax",
path="/",
max_age=token_data.get("refresh_expires_in", 2592000),
)
response.set_cookie(
key="id_token",
value=id_token,
httponly=True,
secure=True,
samesite="lax",
path="/",
max_age=token_data.get("expires_in", 3600),
)
logger.info(f"User {user_id} logged in successfully")
return response
except Exception as e:
logger.error(f"Ошибка обработки callback'а логина: {str(e)}")
raise HTTPException(status_code=401, detail="Ошибка авторизации")
Этот эндпоинт обрабатывает callback от Keycloak после успешной авторизации. Здесь происходит следующее:
получаем токены,
получаем информацию о пользователе,
сохраняем пользователя в БД (если он новый),
устанавливаем куки с токенами,
обрабатываем возможные ошибки.
Обратите внимание: это GET
-эндпоинт, а не POST
. Это важно, так как именно на этот адрес Keycloak перенаправит пользователя после успешной авторизации, передав code
в параметрах запроса.
Ещё одна особенность — возможна ситуация, при которой Keycloak возвращает ошибку вместо кода (например, если пользователь долго не вводил логин или не подтвердил почту). Такой сценарий тоже предусмотрен: если в запросе есть error
, или отсутствует code
, возвращается ошибка 401.
Позже, в главном файле приложения, мы настроим обработку подобных ошибок: в случае ошибок авторизации будет происходить автоматическая переадресация на главную страницу приложения.
Теперь подробнее разберём последовательность действий при успешной авторизации:
Этап 1 — Получение токенов
token_data = await keycloak.get_tokens(code)
access_token = token_data.get("access_token")
refresh_token = token_data.get("refresh_token")
id_token = token_data.get("id_token")
Этот метод был описан ранее: мы отправляем код в Keycloak и получаем в ответ три токена. Все они обязательны, иначе выбрасываем HTTPException
.
Этап 2 — Получение информации о пользователе
user_info = await keycloak.get_user_info(access_token)
user_id = user_info.get("sub")
Тут используем access_token
, чтобы получить данные пользователя. Самое важное поле в ответе — это sub
, оно содержит ID пользователя. Также возвращаются email, подтверждение почты и другая информация.
Этап 3 — Проверка пользователя
users_dao = UsersDAO(session)
user = await users_dao.find_one_or_none_by_id(user_id)
if not user and isinstance(user_info, dict):
user_info["id"] = user_info.pop("sub")
await users_dao.add(AddUser(**user_info))
Если пользователь с таким ID не найден — создаём его в базе. Здесь применяется подход BaseDAO
.
Этап 4 — Установка токенов в куки и редирект
response = RedirectResponse(url="/protected")
response.set_cookie(...)
logger.info(f"User {user_id} logged in successfully")
return response
Куки устанавливаются с параметрами httponly
, secure
, samesite="lax"
. Последний параметр (samesite
) стоит менять на "none"
, если фронт и бэк находятся на разных доменах, чтобы избежать CORS-проблем.
После установки куков — редирект на защищённую страницу приложения.
Теперь реализуем метод выхода из системы:
@router.get("/logout", include_in_schema=False)
async def logout(request: Request):
id_token = request.cookies.get("id_token")
params = {
"client_id": settings.CLIENT_ID,
"post_logout_redirect_uri": settings.BASE_URL,
}
if id_token:
params["id_token_hint"] = id_token
keycloak_logout_url = f"{settings.logout_url}?{urlencode(params)}"
response = RedirectResponse(url=keycloak_logout_url)
response.delete_cookie(
key="access_token",
httponly=True,
secure=True,
samesite="lax",
path="/",
)
response.delete_cookie(
key="id_token",
httponly=True,
secure=True,
samesite="lax",
path="/",
)
response.delete_cookie(
key="refresh_token",
httponly=True,
secure=True,
samesite="lax",
path="/",
)
return response
Этот эндпоинт делает следующее:
получает
id_token
из куков,формирует ссылку для выхода в Keycloak (logout endpoint),
очищает куки (в том числе
access_token
,refresh_token
иid_token
),редиректит пользователя на
post_logout_redirect_uri
.
Важно: мы не просто очищаем куки, но и выполняем выход из сессии Keycloak. Это гарантирует завершение сессии как на клиенте, так и на стороне Identity-провайдера.
Реализуем API-методы веб-приложения
Напоминаю, что мы разрабатываем веб-приложение для заметок. Поэтому нам нужно реализовать API-методы, которые обеспечат базовые CRUD-операции.
Начнём с метода добавления новой заметки:
@router.post("/notes")
async def add_note(
note: AddNote,
user: dict = Depends(get_current_user),
session: AsyncSession = Depends(get_session_with_commit),
):
notes_dao = NotesDAO(session)
await notes_dao.add(AddNoteWithUserId(user_id=user["sub"], **note.model_dump()))
return {"status": "ok", "message": "Note added successfully", "note": note}
Помимо самой заметки, здесь мы используем две подготовленные ранее зависимости FastAPI:
get_current_user
— функция, проверяющая наличие и валидностьaccess_token
в системе. По сути, это механизм аутентификации пользователя;get_session_with_commit
— зависимость, возвращающая асинхронную сессию SQLAlchemy с автоматическим коммитом (т.е. изменения будут автоматически сохранены в базе данных после завершения запроса).
Затем применяем подход BaseDAO
, который я подробно рассматривал в предыдущих статьях и видео. В результате — заметка добавляется в базу, и в ответе возвращается информация об успешной операции.
Следующий эндпоинт предназначен для получения всех заметок текущего пользователя:
@router.get("/notes")
async def get_notes(
user: dict = Depends(get_current_user),
session: AsyncSession = Depends(get_session_without_commit),
):
notes_dao = NotesDAO(session)
notes = await notes_dao.find_all(SUserId(user_id=user["sub"]))
return {"status": "ok", "notes": notes}
Здесь снова используем зависимость для аутентификации пользователя и сессию с базой данных — на этот раз без автоматического коммита (get_session_without_commit
), так как мы только читаем данные и не вносим изменений.
Теперь реализуем метод удаления заметки:
@router.delete("/notes/{note_id}")
async def delete_note(
note_id: str,
user: dict = Depends(get_current_user),
session: AsyncSession = Depends(get_session_with_commit),
):
notes_dao = NotesDAO(session)
note = await notes_dao.find_one_or_none_by_id(note_id)
if not note:
raise HTTPException(status_code=404, detail="Заметка не найдена")
if note.user_id != user["sub"]:
raise HTTPException(
status_code=403, detail="У вас нет прав на удаление этой заметки"
)
await session.delete(note)
return {"status": "ok", "message": "Заметка успешно удалена"}
Здесь мы используем как DAO-подход, так и «чистые» возможности SQLAlchemy. Сначала ищем заметку по ID, а затем, если всё в порядке, удаляем её напрямую через сессию.
Обратите внимание:
если заметка не найдена — возвращается 404,
если пользователь пытается удалить чужую заметку — возвращается 403.
Добавим метод редактирования заметки:
@router.put("/notes/{note_id}")
async def update_note(
note_id: int,
note: AddNote,
user: dict = Depends(get_current_user),
session: AsyncSession = Depends(get_session_with_commit),
):
notes_dao = NotesDAO(session)
existing_note = await notes_dao.find_one_or_none_by_id(note_id)
if not existing_note:
raise HTTPException(status_code=404, detail="Заметка не найдена")
if existing_note.user_id != user["sub"]:
raise HTTPException(
status_code=403, detail="У вас нет прав на обновление этой заметки"
)
existing_note.title = note.title
existing_note.content = note.content
return {"status": "ok", "message": "Заметка успешно обновлена", "note": note}
Здесь снова комбинируем подход BaseDAO
и работу напрямую с объектом SQLAlchemy. Мы сначала получаем объект заметки, проверяем права доступа, затем обновляем поля и возвращаем ответ.
На этом реализация API-методов завершена. В следующей главе займёмся фронтендом приложения.
Реализация фронтенда приложения
Поскольку статья и так получилась довольно объёмной, а детальный разбор фронтенд-разработки не является её основной целью, здесь я ограничусь лишь ключевыми моментами. Главное — показать, как интегрировать простой интерфейс с использованием стандартных средств Python и FastAPI.
Технологии
Фронтенд в нашем приложении реализован с использованием следующих инструментов:
Jinja2 — шаблонизатор для генерации HTML;
Bootstrap — CSS-фреймворк для быстрой стилизации;
HTML, CSS и JavaScript — для базовой логики и визуального оформления.
В качестве сервера используется FastAPI, который через свои эндпоинты будет отдавать шаблоны HTML.
Подготовка структуры
Для начала создадим структуру фронтенд-части:
В папке
app
создаём директориюpages
, а в ней файлrouter.py
— здесь будет описана логика отображения страниц.Создадим папку
templates
в корнеapp
, где будут храниться HTML-шаблоны.Создадим папку
static
в корнеapp
, где будут храниться css и JavaScript файлы.
Импорты и инициализация
В файле router.py
подключаем необходимые зависимости:
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from app.services.auth_dep import get_current_user
Здесь появился новый элемент — Jinja2Templates
. Он отвечает за рендеринг HTML-шаблонов с возможностью передачи переменных в шаблон. Это особенно удобно в небольших проектах. В более масштабных системах фронтенд чаще выносится отдельно (например, на React, VueJS или Angular).
Инициализируем роутер и шаблонизатор:
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
Эндпоинт для главной страницы
Создаём первый маршрут, который будет обрабатывать запросы на корневой адрес /
:
@router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: dict = Depends(get_current_user)):
return RedirectResponse(url="/protected")
Здесь используется зависимость get_current_user
, проверяющая авторизацию пользователя. Если пользователь уже аутентифицирован, происходит переадресация на защищённую страницу. В противном случае Keycloak автоматически перенаправит его на страницу входа.
Обратите внимание: технически у нас нет полноценной «главной страницы» — только редирект. Однако это не единственный возможный подход. Ничего не мешает вам реализовать полноценный лэндинг с кнопкой «Войти», которая при нажатии инициирует переход в Keycloak.
Эндпоинт для защищённой страницы
Добавим второй маршрут, который будет отображать основное содержимое после авторизации:
@router.get("/protected", response_class=HTMLResponse)
async def protected_page(request: Request, user: dict = Depends(get_current_user)):
return templates.TemplateResponse("index.html", {"request": request, "user": user})
Здесь мы передаём данные о пользователе в HTML-шаблон index.html
с помощью Jinja2. Это позволяет динамически отображать информацию, связанную с текущим пользователем.
Что дальше?
Со стороны бэкенда всё готово для отображения пользовательского интерфейса. Следующим шагом станет создание HTML-шаблона, который и будет визуализировать полученные данные. Этим мы займёмся далее.
Обзор фронтенда: HTML + JavaScript
Интерфейс приложения заметок реализован с использованием стандартного HTML, фреймворка Bootstrap 5.3, шаблонизатора Jinja2 и небольшого количества кастомных стилей и скриптов. Всё это делает интерфейс простым, но при этом удобным и функциональным.


Общая структура
Файл index.html
содержит следующие ключевые элементы:
-
Подключения стилей и шрифтов:
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" /> <link rel="stylesheet" href="/static/style.css" /> <link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet" />
-
Шапка с заголовком и кнопкой выхода:
<header class="bg-dark text-white p-3 text-center mb-4"> <div class="d-flex justify-content-between align-items-center"> <h1 class="mb-0">Mimo Заметки</h1> <a href="/api/logout" class="btn btn-outline-light">Выйти</a> </div> </header>
-
Форма добавления новой заметки:
<div class="note-input-container bg-light p-4 rounded shadow-sm mb-5"> <input type="text" id="note-title-input" class="form-control" placeholder="Введите заголовок..." /> <textarea id="note-desc-input" class="form-control" rows="4" placeholder="Введите описание..."></textarea> <button id="add-note-btn" class="btn btn-primary float-end">Добавить заметку</button> </div>
-
Секция со списком заметок, которые будут отрисовываться динамически через JavaScript:
<div id="notes-list" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-4"> <!-- Сюда будут добавляться карточки --> </div>
-
Модальные окна:
Для просмотра заметки;
Для редактирования;
Для подтверждения удаления.
Каждое из них оформлено в едином стиле и вызывается по нужному событию (клик по кнопке и т.д.).
JavaScript: логика взаимодействия
Файл script.js
, подключенный с атрибутом type="module"
, отвечает за динамическое добавление, редактирование и удаление заметок, а также за обработку кликов и взаимодействие с API:
<script type="module" src="/static/script.js"></script>
Пример логики добавления новой заметки:
document.getElementById("add-note-btn").addEventListener("click", async () => {
const title = document.getElementById("note-title-input").value.trim();
const description = document.getElementById("note-desc-input").value.trim();
if (!title || !description) return;
await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, description })
});
// Очистка полей и обновление списка
});
Также реализовано:
Получение списка заметок через
GET /api/notes
;Заполнение и показ модальных окон;
Сохранение изменений;
Удаление заметки с подтверждением.
JavaScript использует fetch API и модульный подход без лишних зависимостей.
Статические ресурсы
Создаём папку static
внутри app
:
app/
├── static/
│ ├── style.css ← пользовательские стили
│ └── script.js ← логика клиентской части
В style.css
можно оформить тени, фон, отступы и любой другой UI-декор, чтобы всё выглядело аккуратно.
Не увидел смысла в более детальном и глубоком рассмотрении JavaScript и HTML кода. Полный исходный код проекта, в том числе с полными стилями, HTML, JS и всем Python кодом вы найдете тут.
Теперь, когда все ключевые части проекта готовы — реализованы API-методы, подключён Keycloak, написан интерфейс — остаётся лишь собрать всё воедино и запустить FastAPI-приложение.
Главный файл приложения и запуск проекта
В этой главе мы разберём структуру файла app/
main.py
, который отвечает за инициализацию приложения, подключение маршрутов, работу с Keycloak и запуск сервера.
Импорты и подготовка
Вначале подключим все необходимые зависимости:
from contextlib import asynccontextmanager
import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from app.config import settings
from app.services.keycloak_client import KeycloakClient
from app.api.router import router as api_router
from app.pages.router import router as pages_router
Здесь мы импортируем как встроенные компоненты FastAPI, так и кастомные модули проекта: настройки, клиент Keycloak, роутеры API и страниц.
Жизненный цикл приложения
Одной из сильных сторон FastAPI является возможность управления жизненным циклом приложения. Мы воспользуемся этим, чтобы:
инициализировать HTTP-клиент
httpx
при старте приложения;передать его в инстанс
KeycloakClient
;сохранить клиент в
app.state
, чтобы использовать его в любом месте приложения;корректно закрыть соединения при завершении работы.
@asynccontextmanager
async def lifespan(app: FastAPI):
#Создаем и сохраняем shared httpx клиент
http_client = httpx.AsyncClient()
app.state.keycloak_client = KeycloakClient(http_client)
#Подключаем роутеры и статику
app.include_router(pages_router)
app.include_router(api_router)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
yield
#акрываем httpx клиент
await http_client.aclose()
Такой подход избавляет нас от необходимости вручную создавать и передавать клиент в каждом эндпоинте.
Теперь создаём приложение с передачей lifespan
:
app = FastAPI(lifespan=lifespan)
Обработка ошибок: редирект при 401
Очень удобное решение — глобально перехватывать ошибку 401 (Unauthorized) и автоматически перенаправлять пользователя на страницу входа Keycloak. Это избавляет нас от необходимости в каждой ручке писать проверки авторизации вручную.
@app.exception_handler(HTTPException)
async def auth_exception_handler(request: Request, exc: HTTPException):
if exc.status_code == 401:
return RedirectResponse(
f"{settings.auth_url}"
f"?client_id={settings.CLIENT_ID}"
f"&response_type=code"
f"&scope=openid"
f"&redirect_uri={settings.redirect_uri}"
)
raise exc
Настройка CORS
Для корректной работы клиентской части, особенно если вы тестируете её с другого домена или порта, обязательно настроим CORS (Cross-Origin Resource Sharing):
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Для продакшена рекомендуется указывать точные домены вместо
"*"
, чтобы избежать уязвимостей.
Запуск проекта
Остаётся только запустить приложение. Выполните следующую команду в терминале:
uvicorn app.main:app --host 0.0.0.0 --port 8000
uvicorn
— ASGI-сервер, который мы использовали как зависимость;app.main:app
— путь до экземпляра FastAPI;--host 0.0.0.0
— позволяет подключаться к приложению извне (например, в Docker);--port 8000
— порт, на котором будет доступен API.
После запуска откройте браузер по адресу http://localhost:8000. Затем создайте туннель, чтобы ваше приложение стало доступным извне и можно было интегрировать проект с Keycloak. Если используете Ngrok, введите команду для запуска.
ngrok http 8000
Полученную ссылку уже привязываем к Keycloack.
Видеообзор проекта
Чтобы лучше понять структуру проекта и увидеть, как всё работает на практике, вы можете посмотреть небольшой видеообзор, который я подготовил. В нём показан весь путь: от авторизации до выполнения запросов к API.
Готовый проект доступен по ссылке: https://fastapikeycloack-yakvenalex.amvera.io
Что дальше?
Чтобы считать проект полностью завершённым, остался лишь последний шаг — разместить его в удалённом доступе. Это позволит каждому, у кого есть ссылка, сразу начать им пользоваться. Ранее мы уже развернули в Amvera Cloud базу данных PostgreSQL и сервис аутентификации Keycloak. Теперь настало время добавить туда и наше веб-приложение — процесс займёт всего несколько минут.
Деплой проекта
Существует два основных подхода к удалённому запуску проектов:
VPS или выделенный сервер — максимальная гибкость, но требует навыков администрирования и настройки.
Облачные хостинги с автодеплоем — быстрый старт, минимум ручной работы.
Если у вас нет большого опыта в работе с сервером или просто хочется сэкономить время, отличным выбором будет использование специализированных облачных платформ. В этом разделе мы развернём проект на Amvera Cloud — удобном сервисе, поддерживающем автодеплой и простую настройку окружения.
Шаги деплоя на Amvera Cloud
1. Регистрация и авторизация
Переходим на сайт Amvera и проходим регистрацию. Обратите внимание, на этом сервисе для авторизации используется Keycloak.
2. Создание проекта
После входа в систему:
Нажимаем “Создать проект”.
В качестве типа сервиса выбираем “Приложение”.
Жмём “Далее”.
3. Настройка проекта
Придумываем название проекта.
Выбираем тариф не ниже “Начальный”, так как более лёгкие тарифы могут не поддерживать нужный функционал.
4. Загрузка файлов
На следующем этапе выбираем способ загрузки проекта:
Через Git (Amvera предоставит подробную инструкцию на экране загрузки файлов)
Или напрямую загружаем файлы проекта, выбрав вкладку "Через интерфейс"
После загрузки нажимаем “Далее” и переходим к базовой конфигурации.
5. Конфигурация приложения
На этом этапе необходимо заполнить параметры конфигурации, чтобы платформа знала, как запускать ваше приложение. Заполните, как указано на скриншоте ниже:

6. Настройка домена
После создания проекта:
Переходим во вкладку “Домены”.
Выбираем бесплатный поддомен от Amvera или привязываем собственный.

7. Указание домена в .env
В переменной BASE_URL
вашего .env
-файла необходимо указать полученный домен. В моем случае:
BASE_URL="https://fastapikeycloack-yakvenalex.amvera.io"
8. Настройка клиента в Keycloak
Открываем панель управления Keycloak и прописываем в конфигурации клиента тот же BASE_URL
, что и выше. Это обеспечит корректную работу авторизации.

9. Пересборка проекта
Заменяем .env файл во вкладке "Репозиторий" на .env файл с актуальной ссылкой и кликаем на "Пересобрать".

Готово!
Теперь вы можете перейти по своему домену и увидеть страницу входа в админку. Деплой завершён — ваш проект официально работает в облаке.
Заключение
Надеюсь, мне удалось познакомить вас с такой мощной и гибкой технологией, как Keycloak, на примере её интеграции с бэкендом на FastAPI. Мы рассмотрели основные этапы настройки аутентификации, конфигурации клиента, а также процесс деплоя проекта в облако. Конечно, система управления доступом и авторизации значительно глубже и масштабнее, чем это возможно охватить в рамках одной статьи.
Если этот материал оказался для вас полезным и вы хотели бы увидеть продолжение — более продвинутые примеры, интеграцию с другими сервисами, кастомизацию UI Keycloak или реализацию дополнительных сценариев безопасности — обязательно дайте знать. Оценка, комментарий или подписка — простой способ показать, что тема вам интересна и стоит дальнейшего развития.
Полный исходный код проекта — как фронтенд, так и бэкенд — доступен в моём Telegram-канале «Лёгкий путь в Python», где уже собралось более 3500 разработчиков. Мы делимся опытом, обмениваемся идеями и просто поддерживаем друг друга в обучении. Присоединяйтесь — это абсолютно бесплатно и, надеюсь, полезно для вашего профессионального роста.
На этом всё. Спасибо за внимание — и до новых встреч!
Комментарии (5)
Heggi
10.05.2025 09:37Очень интересно как устроена система ролей.
Давно смотрю на Keycloak, но не понимаю как его правильно готовить, если само приложение multi tenant, причем могут быть пользователи с разным уровнем прав и с доступами к разным tenant.
А еще, чтобы одни пользователи (с определенной ролью) могли назначать роли другим пользователям.
AlexXYZ
10.05.2025 09:37Не часто попадаются такие объёмные статьи по keycloak. Очень много полезной информации.
Мне кажется, что хорошо бы явно указать, что вы пользуетесь токенами JWT. Из контекста статьи это не очень понятно, т.к. есть и другие способы. У меня на работе тоже пользовались токенами JWT, но идея перевести web-приложения на Cookies мне кажется более практичной (или более близкой к типу аутентификации типа NTLM/Kerberos в корпоративной среде). JWT скорее всего нужно там, где Cookies не применимы, например, вне контекстов браузеров, в приложениях mobile/desktop. Кстати, использование только cookies для web-приложений позволяет избежать лишних проверок/нагрузок на сервер keycloak - если cookie есть, то пользователь аутентифицирован, а если что, то сессию всегда можно удалить на сервере и кука станет невалидна и пользователь не сможет ничего сделать без повторной аутентификации.
Соответственно пара вопросов в сравнении с аутентификацией NTLM/Kerberos:
1.Хотелось бы узнать ваше мнение по поводу работы по cookies для web-приложений.
2.У вас написано, что "Если токена нет или он просрочен — редиректим на страницу входа". Это хорошо работает, когда у вас в форме на странице один-два параметра, которые не трудно ввести повторно, но если параметров в форме, например, несколько десятков, то как тогда бороться с этими неожиданными перенаправлениями на аутентификацию? (пользователь же вынужен покинуть страницу с исходной формой ввода). Т.е. для одностраничного web-приложения использование JWT создаёт неудобства (это и пользователю создаёт проблему из-за непредсказуемых переходов и разработчику web-приложения - всё время надо контролировать/проверять ответы от сервера + ещё и следить, что вдруг какой-то админ/друг_админа выдал себе бессрочный JWT-токен, то попробуй поймай его с таким токеном).
P.S.
Немного маловато про realm-ы написано. Realm-ы позволяют автоматически аутентифицировать пользователей в других приложениях этого realm, если пользователь аутентифицирован хотя бы в одном приложении конкретного realm. Это очень удобная фича, которая существенно упрощает аутентификацию в web-приложениях в корпоративной среде. Например, если в двух вкладках одного браузера открыты две формы ввода логина и пароля, то при вводе credentials в одной вкладке произойдёт автоматический вход в приложение в другой вкладке (при открытом окне ввода логина и пароля):Пример аутентификации пользователя в разных приложениях одного realm без прямого перехода в keycloak (в диалоговом окне на странице приложения через iframe. На экране кастомный диалог keycloak [диалоги аутентификации в keycloak можно кастомизировать])
BcTpe4HbIu
10.05.2025 09:37Не стоит хардкодить URL до эндпоинтов сервера аутентификации. Все эти URL описаны в стандартизированным документе openid_configuration. Если использовать его, то в конфигурации остаётся только ссылка на issuer. Например для Google issuer равен https://accounts.google.com. Значит документ с конфигурацией находится в https://accounts.google.com/.well-known/openid-configuration.
Не нужно ходить в user_info на каждый запрос пользователя, это просто убьет KC на сколько нибудь серьёзной нагрузке. Вы же использовали OpenID Connect, значит access_token - это JWT токен, который можно и нужно валидировать локально на вашем бакенде.
YetAnotherUser
Отличная статья про интеграцию с Keycloak. Но для реального проекта не хватает примера работы с refresh token и снятия нагрузки на keycloak сервер - чтобы не долбить его запросами каждый раз.
yakvenalex Автор
Согласен с замечанием. В продолжении, если тема пойдет, обязательно расскажу об этом.