По сути, токены — это ключи доступа. Они позволяют вашему приложению понять: кто вы, какие права у вас есть, и ещё убедиться, что вы не злоумышленник. Это особенно важно для API-запросов, где сервер не хранит состояние между запросами, а о вас знает только по переданному токену.
Представьте, что вы пытаетесь попасть в закрытый клуб. Без ключа (читай: токена) вы просто останетесь за дверью. С правильным ключом, но просроченным, вас попросят обновить его (как рефреш-токен). Наша задача — сделать этот процесс автоматическим, но безопасным.
Устройство токенов
Вспомним, как выглядит JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjY3OTEyMzQ1fQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Он состоит из трёх частей:
- Header — шапка токена, указывающая на алгоритм подписи (например, HMAC SHA256).
- Payload — полезные данные (id пользователя, время истечения, права доступа).
- Signature — подпись, чтобы никто не мог изменить данные в payload.
Подробное объяснение этой структуры мы уже разбирали, так что перейдём к практике.
Генерация токенов
Для начала нам нужна библиотека PyJWT. Если вы её ещё не установили, сделайте это:
pip install pyjwt
Пусть у нас есть пользователь с уникальным user_id. Мы создадим для него токен, который будет действителен в течение часа.
import jwt
from datetime import datetime, timedelta
# Секретный ключ для подписи токенов (должен быть защищённым!)
SECRET_KEY = "supersecretkey"
def create_access_token(user_id: int):
# Устанавливаем время истечения (через 1 час)
expiration = datetime.utcnow() + timedelta(hours=1)
# Собираем payload
payload = {
"sub": user_id, # Уникальный идентификатор пользователя
"exp": expiration # Время истечения токена
}
# Генерируем JWT
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return token
# Пример использования
token = create_access_token(user_id=42)
print(f"Access token: {token}")
Этот токен можно передавать клиенту. Клиент, в свою очередь, будет отправлять его в заголовке Authorization при запросах к нашему API.
Пара слов о безопасности
Ваш SECRET_KEY — это самое ценное для подписи и проверки токенов. Если он утечёт, токены можно будет подделать. Храните его в переменной окружения или в специальном менеджере секретов (например, Vault).
Проверка и валидность токенов
Теперь научимся проверять токены, которые передаются в запросах. Мы будем декодировать их и проверять:
- Подпись токена.
- Срок действия (expiration).
Базовый пример проверки токена
def verify_access_token(token: str):
try:
# Раскодируем токен
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload # Возвращаем полезные данные из токена
except jwt.ExpiredSignatureError:
raise Exception("Токен истёк")
except jwt.InvalidTokenError:
raise Exception("Недействительный токен")
# Пример использования
try:
payload = verify_access_token(token)
print(f"Token payload: {payload}")
except Exception as e:
print(f"Ошибка проверки токена: {e}")
Работа с токенами в FastAPI
Давайте внедрим всё это в приложение на FastAPI. Мы защитим эндпоинты с помощью токенов. Для этого потребуется:
- Генерация токенов при входе пользователя.
- Проверка токена для доступа к защищённым маршрутам.
Мы создадим эндпоинт /login, который выдаёт токен после проверки учётных данных пользователя.
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordRequestForm
app = FastAPI()
# Фейковая база данных
users_db = {"user@example.com": {"password": "password123", "id": 1}}
@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users_db.get(form_data.username)
if not user or user["password"] != form_data.password:
raise HTTPException(status_code=400, detail="Неверные учетные данные")
access_token = create_access_token(user_id=user["id"])
return {"access_token": access_token, "token_type": "bearer"}
Теперь клиент отправляет данные учётной записи (логин и пароль) и получает токен доступа.
Защита эндпоинтов
Чтобы защитить API, мы будем проверять токен перед выполнением запроса. FastAPI предоставляет класс OAuth2PasswordBearer, который извлекает токен из заголовка Authorization.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
@app.get("/protected")
def protected_route(token: str = Depends(oauth2_scheme)):
payload = verify_access_token(token)
return {"message": f"Привет, пользователь {payload['sub']}!"}
Если токен истёк или недействителен, пользователь получит ошибку.
Работа с рефреш-токенами
Рефреш-токены позволяют обновлять токены доступа без необходимости входа в систему. Схема работы:
- Пользователь получает два токена:
access_tokenиrefresh_token. - Когда
access_tokenистекает, клиент отправляетrefresh_tokenдля получения новогоaccess_token.
Реализация
def create_refresh_token(user_id: int):
expiration = datetime.utcnow() + timedelta(days=7)
payload = {
"sub": user_id,
"exp": expiration
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
@app.post("/refresh")
def refresh_token(refresh_token: str):
try:
payload = verify_access_token(refresh_token)
new_access_token = create_access_token(user_id=payload["sub"])
return {"access_token": new_access_token, "token_type": "bearer"}
except Exception as e:
raise HTTPException(status_code=401, detail="Неверный refresh токен")
Типичные ошибки и их предотвращение
- Истёкшие токены. При проверке токена можно получить ошибку
jwt.ExpiredSignatureError. Это нормальное поведение, так как срок действия токена уже истёк. Добавьте обработчик этой ошибки, чтобы клиент понимал, что нужно обновить токен. - Неверные токены. Если токен был подделан или повреждён, библиотека выбрасывает
jwt.InvalidTokenError. Не забудьте аккуратно обрабатывать эту ошибку. - Безопасность ключей. Никогда не захардкодивайте секретный ключ в коде. Используйте переменные окружения.
Этот подход к работе с токенами позволяет строить надёжные и безопасные системы аутентификации. Закрепите материал, добавляя токены в ваши тестовые API и проверяя их на практических кейсах.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ