По суті, токени — це ключі доступу. Вони дозволяють вашому застосунку зрозуміти: хто ви, які права у вас є, і ще переконатися, що ви не зловмисник. Це особливо важливо для API-запитів, де сервер не зберігає стан між запитами, а про вас знає лише за переданим токеном.
Уявіть, що ви намагаєтеся потрапити в закритий клуб. Без ключа (читай: токена) ви просто залишитесь за дверима. З правильним ключем, але простроченим, вас попросять оновити його (як refresh-токен). Наша задача — зробити цей процес автоматичним, але безпечним.
Пристрій токенів
Згадаймо, як виглядає 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']}!"}
Якщо токен прострочений або недійсний, користувач отримає помилку.
Робота з refresh-токенами
Refresh-токени дозволяють оновлювати токени доступу без необхідності входу в систему. Схема роботи:
- Користувач отримує два токени:
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 і перевіряючи їх на практичних кейсах.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ