Аутентификация: OAuth2, токены и JWT
Аутентификация в FastAPI строится на зависимостях: OAuth2PasswordBearer извлекает токен из заголовка, а зависимость get_current_user проверяет JWT и возвращает пользователя — защита эндпоинта сводится к одному параметру.
Идея: вход выдаёт подписанный токен (JWT), а каждый защищённый эндпоинт просто требует зависимость «текущий пользователь», которая этот токен проверяет. Пароли при этом хранятся только в виде хеша.
Классический поток — OAuth2 Password Flow с JWT. Пользователь присылает логин и пароль на /token; сервер сверяет пароль с хешем в базе и, если всё верно, выдаёт подписанный JWT с временем жизни. Дальше клиент шлёт этот токен в заголовке Authorization: Bearer ..., а сервер на каждом защищённом эндпоинте проверяет подпись и срок действия. Никаких сессий на сервере — токен самодостаточен.
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Annotated
import jwt
app = FastAPI()
oauth2 = OAuth2PasswordBearer(tokenUrl="token")
SECRET = "..."
@app.post("/token")
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = authenticate(form.username, form.password) # сверяем хеш
if not user:
raise HTTPException(status_code=401, detail="неверные данные")
token = jwt.encode({"sub": user.id}, SECRET, algorithm="HS256")
return {"access_token": token, "token_type": "bearer"}
def get_current_user(token: Annotated[str, Depends(oauth2)]):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="невалидный токен")
return load_user(payload["sub"])
CurrentUser = Annotated[dict, Depends(get_current_user)]
@app.get("/me")
async def me(user: CurrentUser):
return user
Два кита безопасности здесь — хеширование паролей и подпись токенов. Пароль никогда не хранится открытым: при регистрации считается его хеш (bcrypt/argon2), при входе сверяется хеш введённого. JWT подписывается секретом: клиент не может подделать содержимое, не зная ключа, а сервер проверяет подпись и срок.
Как работает под капотом
JWT — это base64-кодированные «полезные данные» плюс подпись от них. Подделать данные нельзя, не зная секрета, потому что подпись перестанет сходиться. Смоделируем суть подписи и проверки на stdlib (HMAC):
import hmac, hashlib, json, base64
SECRET = b"super-secret-key"
def sign(payload: dict) -> str:
body = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
sig = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
return f"{body}.{sig}"
def verify(token: str):
body, sig = token.split(".")
expected = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected): # защита от подмены
return None
return json.loads(base64.urlsafe_b64decode(body))
token = sign({"sub": 42, "role": "user"})
print("токен:", token[:40], "...")
print("проверка валидного:", verify(token))
# попытка подделки: меняем тело, подпись не сойдётся
forged = token.replace("sub", "sub")[:-3] + "bad"
print("проверка подделки:", verify(forged))
Попробуй сам ▶ Подделанный токен не проходит проверку, потому что HMAC-подпись не совпадает. Это и есть суть защиты JWT.
Частые ошибки
Первая и тяжёлая — хранить пароли в открытом виде или со слабым/самописным хешированием вместо bcrypt/argon2. Вторая — держать секрет JWT в коде/репозитории вместо переменных окружения. Третья — выдавать токены без срока жизни (exp), делая их вечными. Четвёртая — забывать перехватывать ошибку декодирования и отдавать 500 вместо 401. Пятая — слать токены по HTTP без TLS, открывая их перехвату.
Best practices
- Храните только хеш пароля (bcrypt/argon2), сверяйте при входе.
- Секрет JWT — в переменных окружения, не в коде; токены с обязательным
exp. - Защиту эндпоинта сводите к зависимости
get_current_user; не дублируйте проверку токена. - Всегда работайте по HTTPS; на невалидный токен возвращайте 401, а не 500.
Авторизация: роли и права после аутентификации
Важно различать два понятия, которые часто смешивают. Аутентификация отвечает на вопрос «кто ты» (проверка токена, установление личности), авторизация — «что тебе можно» (проверка прав на действие). JWT решает первую задачу: подтверждает, что запрос от конкретного пользователя. Вторую решают поверх: зависимость get_current_user устанавливает личность, а дальше другие зависимости проверяют роли и права — например, require_admin допускает только администраторов. Удобно строить эти проверки как зависимости-фабрики, накладывая их на отдельные эндпоинты или целые роутеры. В сам токен иногда кладут роли (claim scopes), чтобы не ходить за ними в базу на каждый запрос, но тогда важно помнить о времени жизни: пока токен не истёк, права в нём «заморожены». Понимание границы «аутентификация против авторизации» избавляет от путаницы и дыр в безопасности, когда личность проверена, а права — нет.
Итог: аутентификация в FastAPI — это связка «вход выдаёт подписанный JWT» и «зависимость проверяет его на каждом защищённом эндпоинте». Пароли хранятся хешами, токены подписываются секретом и имеют срок; защита маршрута — один параметр-зависимость.