Skip to content

Repository files navigation

Система аутентификации и авторизации

Тестовое задание: backend-приложение с собственной аутентификацией и системой разграничения прав доступа. Возможности фреймворка «из коробки» намеренно не используются — django.contrib.auth, django.contrib.sessions и authentication/permission-классы DRF отключены, вместо них написан свой слой.

Стек: Python 3.12, Django 5.1, Django REST Framework, PostgreSQL (SQLite как запасной вариант для быстрого запуска), bcrypt, PyJWT.

Запуск

# вариант 1 — docker (PostgreSQL внутри)
docker compose up --build
# приложение на http://127.0.0.1:8000

# вариант 2 — локально, на SQLite
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py seed        # роли, объекты, правила доступа, тестовые пользователи
python manage.py runserver

Тесты:

python manage.py test tests

Тестовые пользователи создаются командой seed:

email пароль роль
admin@example.com AdminPass123 admin
manager@example.com ManagerPass123 manager
user@example.com UserPass123 user
guest@example.com GuestPass123 guest

Как устроена аутентификация

Пароль никогда не хранится в открытом виде: при регистрации он превращается в bcrypt-хеш с индивидуальной солью (accounts/hashing.py).

После успешного login создаются две сущности:

  1. Серверная сессия — запись в таблице sessions со случайным ключом, сроком жизни и полем revoked_at.
  2. JWT-токен доступа, внутри которого лежат sub (id пользователя) и sid (ключ сессии).

Токен возвращается в теле ответа, а ключ сессии дополнительно ставится в httponly-cookie sessionid — клиент может работать любым из двух способов:

Authorization: Bearer <jwt>
Cookie: sessionid=<ключ>

Связка «JWT + серверная сессия» выбрана намеренно. Чистый JWT нельзя отозвать до истечения срока: после logout или удаления аккаунта старый токен продолжал бы работать. Здесь же каждый запрос сверяется с сессией в базе, поэтому выход из системы и мягкое удаление обрывают доступ мгновенно.

Пользователя определяет собственный middleware (accounts/middleware.py): до обработки запроса он достаёт ключ сессии, проверяет, что сессия не истекла и не отозвана, а владелец активен, и кладёт пользователя в request.auth_user. Отдельный атрибут вместо request.user нужен потому, что DRF подменяет request.user собственным объектом.

Мягкое удаление (DELETE /api/auth/profile/delete) выставляет is_active=False и deleted_at, отзывает все сессии пользователя, но саму запись в базе сохраняет — войти под ней больше нельзя.

Схема разграничения доступа

Три таблицы:

roles — роли: admin, manager, user, guest.

business_elements — объекты приложения, доступ к которым разграничивается: users, products, shops, orders, access_rules.

access_roles_rules — правило на пару «роль × объект», семь булевых флагов:

поле смысл
read_permission читать свои объекты
read_all_permission читать любые объекты
create_permission создавать объекты
update_permission изменять свои объекты
update_all_permission изменять любые объекты
delete_permission удалять свои объекты
delete_all_permission удалять любые объекты

Парность флагов — ключевая идея схемы. «Свой» объект определяется полем owner, ссылающимся на пользователя. Менеджеру можно выдать read_all_permission на заказы, а обычному пользователю — только read_permission, и тот же самый эндпоинт вернёт ему лишь его собственные записи. Так одно правило описывает и факт доступа, и его область видимости, без отдельных ролей вида «менеджер-который-видит-всё».

Проверка живёт в access/permissions.py. Функция check_access(request, element, action) возвращает решение и область видимости:

  • пользователь не определён → 401 Unauthorized;
  • правило отсутствует или флаг выключен → 403 Forbidden;
  • разрешение есть → выдаётся ресурс, отфильтрованный по владельцу, если разрешена только своя область.

Порядок проверки: сначала *_all_permission (даёт полную область), затем обычный флаг (ограничивает своими объектами). Для create понятие владельца неприменимо, поэтому область не сужается.

Матрица прав по умолчанию задаётся командой seed и дальше правится через API администратора — правила лежат в базе, а не в коде, поэтому меняются без перезапуска приложения.

API

Пользователь

метод путь описание
POST /api/auth/register регистрация: имя, фамилия, отчество, email, пароль + повтор
POST /api/auth/login вход по email и паролю, выдаёт токен и cookie
POST /api/auth/logout выход, сессия отзывается
GET /api/auth/profile текущий профиль
PATCH /api/auth/profile изменение имени, фамилии, отчества
DELETE /api/auth/profile/delete мягкое удаление аккаунта

Управление правилами (только роль admin)

метод путь описание
GET, POST /api/admin/roles список и создание ролей
GET, POST /api/admin/business-elements список и создание объектов
GET, POST /api/admin/access-rules список и создание правил
GET, PATCH, DELETE /api/admin/access-rules/<id> чтение, изменение, удаление правила

Бизнес-объекты (mock)

метод путь
GET, POST, PATCH, DELETE /api/products, /api/shops, /api/orders

Таблицы для них по условию задания не создаются: объекты лежат в business/storage.py и содержат поле owner_id, на котором и проверяется разграничение «свои против всех». Ответ на чтение включает поле scope (own или all) — по нему видно, какая область прав сработала.

Примеры

# регистрация
curl -X POST http://127.0.0.1:8000/api/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"ivan@example.com","password":"StrongPass123",
       "password_repeat":"StrongPass123","first_name":"Иван","last_name":"Иванов"}'

# вход
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"UserPass123"}' | jq -r .access_token)

curl http://127.0.0.1:8000/api/orders                              # 401
curl http://127.0.0.1:8000/api/orders -H "Authorization: Bearer $TOKEN"
# 200, "scope":"own" — только заказы этого пользователя

curl http://127.0.0.1:8000/api/admin/access-rules -H "Authorization: Bearer $TOKEN"
# 403 — правилами управляет только администратор

Тесты

16 тестов в tests/test_api.py покрывают: хеширование пароля и отказ при несовпадении паролей, повторный email, выдачу токена и cookie, вход с неверным паролем, доступ по Bearer-токену, отзыв сессии при выходе, мягкое удаление, а также разграничение доступа — 401 без пользователя, 403 без правила, сужение выдачи до своих объектов, полную выдачу по *_all_permission, закрытость административного API и применение изменённого правила на лету.

Структура

accounts/   пользователи, сессии, bcrypt, JWT, middleware, эндпоинты профиля
access/     роли, объекты, правила доступа, движок проверки, админское API
business/   mock-объекты бизнес-приложения
config/     настройки, маршруты, единый формат ошибок
tests/      тесты

About

Собственная система аутентификации и авторизации на Django + DRF: bcrypt, JWT с серверными сессиями, ролевая модель доступа со разграничением «свои объекты / все объекты»

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages