Практика 13. Практическая работа 13. Аутентификация и авторизация (JWT + RBAC)
Раздел 4. Безопасность и работа с пользователями Курс «Разработка интернет-приложений» (backend, Python/FastAPI)
Цель
- Реализовать регистрацию и вход пользователей в FastAPI.
- Научиться хешировать пароли через passlib/bcrypt и никогда не хранить их в открытом виде.
- Выдавать и проверять JWT (python-jose), используя
OAuth2PasswordBearer. - Написать зависимость
get_current_userи защитить маршруты. - Внедрить ролевую модель доступа (RBAC): разграничить права
userиadmin. - Настроить CORS для безопасной работы с фронтендом.
Теория
- Аутентификация отвечает на вопрос «кто вы?» (вход по логину и паролю), авторизация — «что вам разрешено?» (проверка роли). Им соответствуют коды
401 Unauthorizedи403 Forbidden. - HTTP не хранит состояние, поэтому после входа клиент с каждым запросом шлёт токен в заголовке
Authorization: Bearer <token>. - JWT состоит из трёх частей
header.payload.signature. Payload не зашифрован (секреты туда не кладём), а подпись поSECRET_KEYгарантирует целостность: подделать токен без ключа нельзя. - Пароли хранят только в виде адаптивного хеша (bcrypt) с автоматической солью, а не через быстрые MD5/SHA.
- RBAC (Role-Based Access Control): пользователю назначается роль, а проверка прав на сервере выполняется через зависимости (
Depends). - CORS разрешает доверенным «чужим» источникам обращаться к API из браузера.
Задание
Подготовка окружения
mkdir fastapi-authcd fastapi-authpython -m venv .venv.\.venv\Scripts\Activate.ps1python -m pip install --upgrade pippip install fastapi uvicorn "passlib[bcrypt]" "python-jose[cryptography]" python-multipart email-validator
python-multipartнужен для формы входаOAuth2PasswordRequestForm,python-jose— для работы с JWT,passlib[bcrypt]— для хеширования паролей.
Задание 1. Каркас приложения, настройки и утилиты
Создайте main.py. Опишите константы, контекст хеширования, схему OAuth2 и «БД» в памяти.
from datetime import datetime, timedeltafrom enum import Enum
from fastapi import FastAPI, Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestFormfrom passlib.context import CryptContextfrom jose import jwt, JWTErrorfrom pydantic import BaseModel, EmailStr
app = FastAPI(title="Auth API (Practice 13)")
SECRET_KEY = "change-me-in-production" # в реальности — из переменной окруженияALGORITHM = "HS256"ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
users_db: dict[str, dict] = {} # упрощённая «БД»; в реальности — PostgreSQL
class Role(str, Enum): ADMIN = "admin" USER = "user"
def hash_password(password: str) -> str: return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed)Задание 2. Модели и регистрация пользователя
Опишите Pydantic-модели и эндпоинт POST /register. Пароль сразу хешируем и в ответе не возвращаем.
class UserCreate(BaseModel): username: str email: EmailStr password: str
class UserOut(BaseModel): username: str email: EmailStr role: Role
@app.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)def register(data: UserCreate): if data.username in users_db: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered", ) user = { "username": data.username, "email": data.email, "hashed_password": hash_password(data.password), "role": Role.USER, # по умолчанию обычный пользователь } users_db[data.username] = user return userЗадание 3. Вход и выдача JWT
Реализуйте функцию создания токена и эндпоинт POST /token. Используйте OAuth2PasswordRequestForm (поля username, password).
def create_access_token(data: dict) -> str: to_encode = data.copy() to_encode["exp"] = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app.post("/token")def login(form_data: OAuth2PasswordRequestForm = Depends()): user = users_db.get(form_data.username) if not user or not verify_password(form_data.password, user["hashed_password"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) token = create_access_token({"sub": user["username"], "role": user["role"].value}) return {"access_token": token, "token_type": "bearer"}В payload кладём
sub(имя пользователя) иrole— роль понадобится для RBAC.
Задание 4. Зависимость get_current_user и защита маршрута
Напишите зависимость, которая достаёт токен, проверяет подпись и срок действия, находит пользователя. Защитите эндпоинт GET /users/me.
def get_current_user(token: str = Depends(oauth2_scheme)) -> dict: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception
user = users_db.get(username) if user is None: raise credentials_exception return user
@app.get("/users/me", response_model=UserOut)def read_me(current_user: dict = Depends(get_current_user)): return current_userПолный флоу: POST /register → POST /token (получить токен) → GET /users/me с заголовком Authorization: Bearer <token>.
Задание 5. RBAC: доступ только для admin
Реализуйте фабрику зависимостей require_role и защищённый админский эндпоинт. Если роль не совпадает — 403 Forbidden.
def require_role(required: Role): def checker(current_user: dict = Depends(get_current_user)) -> dict: if current_user["role"] != required: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав", ) return current_user return checker
@app.get("/admin/users", response_model=list[UserOut])def list_users(admin: dict = Depends(require_role(Role.ADMIN))): # доступно только пользователям с ролью admin return list(users_db.values())Проверка прав вынесена из тела обработчика: чтобы попасть внутрь, нужен валидный JWT и роль
admin.
Задание 6. Назначение роли admin (для проверки RBAC)
Чтобы протестировать админский маршрут, нужен пользователь-админ. Добавьте служебный эндпоинт повышения роли (в реальном проекте так не делают — роли назначает другой администратор или сидер БД).
@app.post("/admin/grant/{username}", response_model=UserOut)def grant_admin(username: str, admin: dict = Depends(require_role(Role.ADMIN))): user = users_db.get(username) if user is None: raise HTTPException(status_code=404, detail="User not found") user["role"] = Role.ADMIN return userДля самого первого админа добавьте «сидер» при старте приложения:
@app.on_event("startup")def seed_admin(): if "root" not in users_db: users_db["root"] = { "username": "root", "email": "root@example.com", "hashed_password": hash_password("root123"), "role": Role.ADMIN, }Задание 7. Настройка CORS
Подключите CORSMiddleware, чтобы фронтенд с доверенного источника мог обращаться к API.
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], # конкретный источник фронтенда allow_credentials=True, allow_methods=["*"], allow_headers=["Authorization", "Content-Type"],)Антипаттерн:
allow_origins=["*"]вместе сallow_credentials=True— спецификация такую комбинацию запрещает. Всегда указывайте явный список доверенных источников.
Запуск и проверка
uvicorn main:app --reload --host 127.0.0.1 --port 8000Откройте Swagger UI: http://127.0.0.1:8000/docs. Проверьте сценарии:
POST /register— зарегистрируйте обычного пользователя.POST /token— войдите, скопируйтеaccess_token(в Swagger удобно нажать кнопку Authorize).GET /users/me— должен вернуть ваши данные; без токена —401.GET /admin/usersобычным пользователем — должен вернуть403.- Войдите как
root/root123, повторитеGET /admin/users— теперь200.
Критерии оценки
- 20% — регистрация с хешированием пароля (passlib/bcrypt); пароль не хранится и не возвращается в открытом виде.
- 20% — вход
POST /tokenс выдачей корректного JWT (python-jose); неверные данные дают401. - 20% — зависимость
get_current_userпроверяет подпись и срок токена; защищённыйGET /users/meработает. - 20% — RBAC:
require_role/проверка роли, админский маршрут отдаёт403обычному пользователю и200админу. - 10% — корректно настроен CORS (явный список источников, без небезопасной комбинации
*+ credentials). - 10% — код запускается без ошибок, проверен через Swagger UI, все 5 сценариев воспроизводятся.
Вопросы для самопроверки
- Чем аутентификация отличается от авторизации? Какие коды HTTP (
401/403) им соответствуют? - Почему пароль хешируют через bcrypt, а не через MD5/SHA-256? Что такое соль и зачем она нужна?
- Из каких трёх частей состоит JWT? Почему в payload нельзя класть секреты?
- Что произойдёт, если изменить payload токена, не зная
SECRET_KEY? - Какую роль играет
OAuth2PasswordBearerи что делает зависимостьget_current_user? - Зачем класть
roleв payload токена и как на её основе строится проверка прав? - Что такое «фабрика зависимостей»
require_roleи почему проверку прав делают на сервере, а не только в интерфейсе? - Зачем нужен CORS и почему комбинация
allow_origins=["*"]сallow_credentials=Trueнебезопасна?