FastAPI: API-ключ в заголовке или cookie без случайного отказа
Служебным API пользуются скрипт и браузерный интерфейс. Скрипт передаёт ключ заголовком, интерфейс — cookie. Разработчик подключает две security-зависимости и ожидает «подойдёт любой способ». Но каждая зависимость может сама прервать запрос при отсутствии своего значения. Получается требование прислать оба credential вместо одного.
Где принимается решение «или»
Поставим auto_error=False у обоих извлекателей. Теперь отсутствие значения возвращает None, и общая зависимость сама решает, какие комбинации разрешены. Выберем явный договор: одно значение допустимо; два одинаковых тоже; два разных дают 400, чтобы не выбирать случайную личность. Отсутствующий или неизвестный ключ даёт 401.
Такой договор полезен при переходе между клиентами. Если молча предпочитать cookie, скрипт с заголовком способен действовать от другой сессии в тестовом браузере. Если предпочитать заголовок, забытый старый ключ меняет результат входа. Отказ конфликта делает неоднозначность видимой до выполнения команды. При другом продуктовом требовании приоритет можно задать явно, сохранив тест на оба значения.
Один endpoint и все комбинации
Установите fastapi и httpx. Ключ здесь учебный и используется только локально. Cookie передаётся через отдельного клиента, чтобы случайное состояние cookie jar не повлияло на соседние сценарии. Все решения принимает та же зависимость, которую реально вызывает маршрут.
from typing import Annotated
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import APIKeyHeader, APIKeyCookie
from fastapi.testclient import TestClient
app = FastAPI()
header_key = APIKeyHeader(name="X-API-Key", auto_error=False)
cookie_key = APIKeyCookie(name="api_key", auto_error=False)
known = {"demo-key": "automation"}
def principal(
header: Annotated[str | None, Depends(header_key)],
cookie: Annotated[str | None, Depends(cookie_key)],
):
if header is not None and cookie is not None and header != cookie:
raise HTTPException(400, "conflicting credentials")
user = known.get(header if header is not None else cookie)
if user is None:
raise HTTPException(401, "API key required")
return user
@app.get("/jobs")
def jobs(user: Annotated[str, Depends(principal)]):
return {"owner": user}
with TestClient(app) as client:
assert client.get("/jobs").status_code == 401
assert client.get("/jobs", headers={"X-API-Key": "wrong"}).status_code == 401
assert client.get("/jobs", headers={"X-API-Key": "demo-key"}).json() == {
"owner": "automation"}
with TestClient(app, cookies={"api_key": "demo-key"}) as client:
ok = client.get("/jobs")
assert ok.status_code == 200 and ok.json() == {"owner": "automation"}
assert client.get("/jobs", headers={"X-API-Key": "demo-key"}).status_code == 200
conflict = client.get("/jobs", headers={"X-API-Key": "other"})
assert conflict.status_code == 400
print({"cookie": ok.json(), "conflict": conflict.status_code})
Cookie меняет условия использования
Браузер может автоматически отправлять cookie. Для изменяющих операций нужна отдельная защита от CSRF в соответствии с архитектурой приложения. Флаги Secure, HttpOnly и SameSite задаются при выдаче cookie; приведённый маршрут только читает уже полученное значение. Возможность входа через cookie не означает, что безопасно добавить её к любому существующему API без анализа способа вызова.
В системе с несколькими клиентами ключ связывают с владельцем, областью доступа и возможностью отзыва. Извлекатель заголовка не выполняет этих проверок. Реестр known показывает точку, где появится обращение к хранилищу или проверка выбранного формата. После этого principal остаётся общим результатом, и маршрутам не нужно знать, откуда пришло подтверждение личности.
Проверьте OpenAPI при изменении схем безопасности: внешний генератор клиента должен видеть разрешённые способы. Дополнительно оставьте HTTP-тесты комбинаций. Схема описывает доступные механизмы, а правило конфликта двух значений является вашей прикладной логикой. Именно его особенно легко потерять при упрощении зависимости до header or cookie.
Для браузерного пользовательского входа оцените отдельную сессию вместо долгоживущего API-ключа. Но для уже выбранного договора правило остаётся простым: извлечь оба значения, убрать неоднозначность, проверить credential, затем решать права действия. Подробнее о последнем шаге — в разборе аутентификации и авторизации.
Сценарий отзыва ключа
В хранилище должен быть явный признак, что credential больше не действует. Получение пользователя по когда-то выданной строке без проверки отзыва оставит доступ после выключения интеграции. Для локального теста удалите ключ из реестра и повторите запрос через оба транспорта: отказ должен быть одинаковым. После теста восстановите состояние, чтобы следующие сценарии не зависели от порядка выполнения.
Отдельно определите смысл пустой строки. В приведённом договоре это переданное, но неизвестное значение. Две разные строки, одна из которых пустая, всё равно считаются конфликтом. Такой выбор не позволяет плохому заголовку незаметно переключить запрос на cookie. Если вы решите игнорировать пустые credentials, отразите это в общей зависимости и тесте комбинации.
Продолжить на практике
Соберите такую общую зависимость в учебном CRUD и проверьте её на нескольких маршрутах. В бесплатной программе FastAPI зависимости соединяются с авторизацией и тестами; это подходящая основа для практики разных клиентов. Откройте бесплатную программу FastAPI.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
