Представьте, что ваш API — это некий клуб, и только участники с правильно выданными пропусками могут туда попасть. JWT в данном случае — это наш "пропуск". Без верификации токена любой пользователь может получить доступ к вашим данным, как будто кто-то пробрался в ваш клуб без приглашения. Чтобы такого не произошло, мы обеспечиваем защиту эндпоинтов, проверяя токен при каждом запросе.
Использование JWT позволяет:
- Обеспечить безопасность, ограничив доступ неавторизованным пользователям.
- Реализовать контроль доступа на основе ролей, если требуется.
- Сделать API удобным для интеграции, так как токены не требуют хранения состояния на сервере.
Основы защиты эндпоинтов
Защита эндпоинтов состоит из двух ключевых операций:
- Проверка JWT: убедиться, что предоставленный токен валиден.
- Ограничение доступа: определить, имеет ли пользователь с предоставленным токеном право доступа к ресурсу.
В FastAPI для этих задач мы используем:
- Механизм
Dependsдля внедрения зависимостей в маршруты. - Проверку JWT через
PyJWTдля верификации подписи и извлечения данных.
Пример ограничения доступа к ресурсам
Начнем с простого примера. Представим, у нас есть защищенный эндпоинт /protected, доступ к которому разрешен только аутентифицированным пользователям.
Шаг 1: создадим файл dependencies.py
Этот модуль будет хранить функции, которые мы будем подключать через Depends.
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer
from jose import jwt, JWTError
SECRET_KEY = "supersecretkey123" # Используйте безопасный ключ в реальном проекте
ALGORITHM = "HS256"
# Определяем схему авторизации через HTTPBearer (заголовок Authorization)
oauth2_scheme = HTTPBearer()
def verify_jwt(token: str):
try:
# Проверяем и декодируем JWT
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload # Возвращаем данные токена (например, user_id)
except JWTError:
raise HTTPException(status_code=401, detail="Неверный токен или токен истек")
def get_current_user(token: str = Depends(oauth2_scheme)):
# Получаем токен из заголовков через HTTPBearer
return verify_jwt(token) # Декодируем и проверяем токен
Шаг 2: ограничим доступ к эндпоинту с помощью зависимости
Создадим защищенный эндпоинт /protected.
from fastapi import FastAPI, Depends
from .dependencies import get_current_user
app = FastAPI()
@app.get("/protected")
def protected_route(current_user: dict = Depends(get_current_user)):
# current_user содержит данные пользователя из токена
return {"message": f"Добро пожаловать, {current_user.get('sub')}!"}
Что здесь происходит?
- В эндпоинте мы подключаем зависимость
get_current_userчерезDepends. get_current_userпроверяет токен черезverify_jwt, выбрасывает ошибку, если токен недействителен, и возвращает данные пользователя (payload).- В случае успеха пользователь получает доступ к эндпоинту.
Настройка декораторов для защиты маршрутов
Часто бывает так, что нам нужно защитить сразу несколько эндпоинтов. Чтобы не дублировать код, можно создать кастомный декоратор.
Приведём пример кастомного декоратора.
from fastapi import Request, HTTPException
def jwt_required(func):
async def wrapper(request: Request, *args, **kwargs):
token = request.headers.get("Authorization")
if not token or not token.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Токен не предоставлен")
try:
token = token.split(" ")[1] # Извлекаем сам токен
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
request.state.user = payload # Сохраняем данные токена в запрос
except JWTError:
raise HTTPException(status_code=401, detail="Неверный токен")
return await func(request, *args, **kwargs)
return wrapper
Теперь, мы можем использовать декоратор так:
@app.get("/protected-decorator")
@jwt_required
async def protected_with_decorator(request: Request):
user = request.state.user
return {"message": f"Привет, {user.get('sub')}!"}
Разделение доступа по ролям
В реальном мире не все пользователи равны. Некоторые могут быть администраторами, другие — обычными пользователями. Давайте добавим проверку ролей в нашу систему.
Шаг 1: обновим функцию проверки токенов
Добавим логическую проверку на наличие нужной роли.
def get_current_admin(token: str = Depends(oauth2_scheme)):
payload = verify_jwt(token.credentials)
if payload.get("role") != "admin":
raise HTTPException(status_code=403, detail="Доступ запрещен: недостаточно прав")
return payload
Шаг 2: использование зависимости для проверки ролей
@app.get("/admin")
def admin_route(current_admin: dict = Depends(get_current_admin)):
return {"message": f"Добро пожаловать в админ-зону, {current_admin.get('sub')}!"}
Теперь доступ к маршруту /admin есть только у пользователей с ролью admin.
Обработка ошибок при проверке токенов
Продолжаем с лучшими практиками: важно быть готовыми к любым неожиданностям. Например, пользователь может отправить:
- Просроченный токен.
- Токен с неправильной подписью.
- Токен с измененными данными.
Все эти случаи уже обрабатываются в функции verify_jwt через выброс исключений HTTPException. Однако, для улучшения отладки, мы можем логировать такие события.
import logging
logger = logging.getLogger(__name__)
def verify_jwt_with_logging(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError as e:
logger.error(f"Ошибка проверки токена: {e}")
raise HTTPException(status_code=401, detail="Неверный токен")
Практическое примечание
В реальных приложениях защита эндпоинтов с использованием JWT часто встречается в следующих сценариях:
- Лимитированный доступ к ресурсам, таким как личные данные пользователя.
- Защита административных маршрутов (например,
/admin,/dashboard). - Внедрение мультиуровневой авторизации с разделением доступа по ролям.
Теперь вы на шаг ближе к созданию защищенных, надежных API. Примеры, которые мы разобрали, легко масштабируются и могут быть адаптированы для любых нужд.
Полезные ссылки:
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ