Для начала — немного теории. JWT-токены имеют ограниченный срок действия. Это сделано для повышения безопасности: если злоумышленник каким-то образом украдёт токен, его "срок жизни" будет ограничен. Но это создаёт неудобства для ваших пользователей: как только срок действия токена истекает, они вынуждены снова вводить свои данные для входа.
Рефреш-токен приходит на помощь. Основные его преимущества:
- Долговременное хранение: рефреш-токен имеет более длительный срок действия.
- Обновление доступа: позволяет пользователю получить новый Access-токен без повторного ввода данных.
- Восстановление сессий: делает пользовательский опыт более плавным.
Рефреш-токены — это как ключ от дома, спрятанный под ковриком. Мы знаем, что он там есть, но пользуемся только в случае необходимости. Главное — чтобы коврик никто не унес.
Реализация рефреш-токенов в FastAPI
Теперь, когда мы разобрались, зачем нужны рефреш-токены, перейдём к практике.
Итак, главная функция рефреш-токена — это обновление Access-токена. Для этого мы создадим новый эндпоинт /token/refresh, который будет принимать рефреш-токен, проверять его валидность и, если всё хорошо, выдавать новый Access-токен.
Начнём с основного кода:
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from datetime import datetime, timedelta
from pydantic import BaseModel
app = FastAPI()
# Секреты и алгоритмы для токенов
SECRET_KEY = "your_secret_key"
REFRESH_SECRET_KEY = "your_refresh_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 15
REFRESH_TOKEN_EXPIRE_DAYS = 7
# OAuth2 схема
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Модель для токенов
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str
# Модель для данных пользователя
class User(BaseModel):
username: str
# Временное хранилище пользователей и рефреш-токенов
users_db = {
"johndoe": {"username": "johndoe", "hashed_password": "fakehashedpassword"}
}
refresh_tokens_store = {}
# Генерация Access-токена
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Генерация Refresh-токена
def create_refresh_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, REFRESH_SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Вход пользователя и генерация токенов
@app.post("/token", response_model=Token)
async def login(username: str, password: str):
user = users_db.get(username)
if not user or password != "password": # Пример проверки, лучше использовать хэш
raise HTTPException(status_code=400, detail="Invalid credentials")
access_token = create_access_token(
data={"sub": user["username"]},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
refresh_token = create_refresh_token(data={"sub": user["username"]})
refresh_tokens_store[user["username"]] = refresh_token
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
# Обновление Access-токена
@app.post("/token/refresh", response_model=Token)
async def refresh_token(refresh_token: str):
try:
# Проверка рефреш-токена
payload = jwt.decode(refresh_token, REFRESH_SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None or refresh_tokens_store.get(username) != refresh_token:
raise HTTPException(status_code=401, detail="Invalid refresh token")
# Если всё хорошо, создаём новый Access-токен
access_token = create_access_token(
data={"sub": username},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
- Хранилище рефреш-токенов:
В данном примере мы храним рефреш-токены в памяти (refresh_tokens_store). Это не лучший подход, поскольку в реальных приложениях рефреш-токены должны храниться безопасно (например, в базе данных). - Создание токенов:
Мы разделили генерациюaccess_tokenиrefresh_token, используя разные секретные ключи (SECRET_KEYиREFRESH_SECRET_KEY). Это повышает безопасность системы. - Эндпоинт
/token/refresh:
Проверяет валидность рефреш-токена.
Генерирует новый Access-токен, используя ту же информацию о пользователе. - Срок действия токенов:
Access-токен живёт 15 минут, тогда как рефреш-токен — целых 7 дней.
Практические замечания
- Безопасное хранение токенов:
- Access-токен может быть сохранён в памяти клиента (например, в
localStorageилиsessionStorage). - Рефреш-токен должен храниться только в
HttpOnlycookie. Это защитит его от атак XSS.
- Access-токен может быть сохранён в памяти клиента (например, в
- Истечение срока рефреш-токена:
При истечении рефреш-токена пользователь будет вынужден войти в систему заново. - Двухфакторная аутентификация:
Включение рефреш-токенов позволяет легко внедрять двухфакторную аутентификацию.
Типичные ошибки
Одной из распространённых ошибок является использование одного и того же секретного ключа для Access и Refresh-токенов. Это повышает риск компрометации системы, так как злоумышленник, получивший доступ к ключу, сможет создавать оба типа токенов.
Теперь ваша система авторизации в FastAPI стала ещё ближе к боевой реальности: пользователи не будут разлогиниваться каждые 15 минут, что сделает их взаимодействие с приложением более плавным, а безопасность сети останется на высоте!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ