Перейти к содержанию

Безопасность API и CORS

HTML-страницы используют действующую сессию, проверяют владельца и защищают изменения токеном. Теперь добавим JSON-API для того же содержания. Другой формат ответа не создаёт другой модели доступа: заметка Alice остаётся личной и при запросе программы.

Результат урока — ограниченный договор чтения и изменения. Мы не разрешаем клиенту выбирать owner_id, имя таблицы или произвольный набор полей. API получает только значения, необходимые конкретной операции.

Объект и маршрут

Для чтения используем /api/notes/1. Helper note_for_user уже определён и выполняет проверку сессии и владельца. Возвращаем только публичные для этого владельца поля заметки.

@app.get("/api/notes/<int:note_id>")
def api_note(note_id):
    item = note_for_user(note_id)
    return jsonify(
        id=item["id"], title=item["title"], text=item["text"]
    )

Password_hash, внутренняя конфигурация и полный объект пользователя здесь отсутствуют. Даже если библиотека сериализации умеет вывести любую строку базы, продукту не требуется такая возможность. Список отображаемых полей является частью API-договора.

Номер заметки не заменяет право. Попытка Boris обратиться к API Alice должна дать такой же отказ по содержанию, что и HTML-маршрут. Это проверяет повторное использование общей границы, а не только корректность JSON.

Изменение JSON

Для обновления используем PATCH с title и text. В маленьком примере оба поля обязательны: это явно заданная операция замены содержательных полей, а не универсальный частичный редактор.

def read_note_json(data):
    if not isinstance(data, dict) or set(data) != {"title", "text"}:
        abort(400, description="Неверные данные")
    if not isinstance(data["title"], str) or not isinstance(data["text"], str):
        abort(400, description="Неверные данные")
    title = data["title"].strip()
    text = data["text"]
    if not 1 <= len(title) <= 120 or len(text) > 4000:
        abort(400, description="Неверные данные")
    return title, text

Булево значение не принимается как название, а вложенный объект не преобразуется молча в строку. Такие преобразования создают неожиданный смысл и затрудняют сопровождение. Читатель должен знать, какое значение реально окажется в базе.

Content-Type проверяется перед разбором. В финальном маршруте неподходящий тип получает 415, неверная структура — 400. В обоих случаях запись не меняется. Общий предел тела из конфигурации применяется независимо от формата.

Сессия и токен

Наш браузер обращается к тому же origin, поэтому он может использовать установленную сессию. Для изменяющего JSON также передаётся X-CSRF-Token. Это значение берётся из собственной страницы, а не создаётся случайно клиентом на каждый запрос.

const token = document.querySelector('input[name="csrf_token"]').value;
const response = await fetch("/api/notes/1", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": token
  },
  body: JSON.stringify({ title: "План Markdown", text: "Новый учебный текст." })
});

Это самостоятельный браузерный фрагмент для страницы с токеном. В полном учебном интерфейсе изменение также доступно обычной формой; отдельный SPA-фреймворк для объяснения API не нужен.

Проверка токена уже выполняется общим before_request. API не получает исключение только из-за application/json. Новая программа не должна обойти старую границу через другой тип тела.

CORS и авторизация

CORS определяет, может ли браузерный код другого origin читать определённый ответ. Он не устанавливает право пользователя на заметку и не ограничивает обычный серверный HTTP-клиент. Поэтому его нельзя считать заменой require_user.

Нашему приложению не требуется браузерный клиент другого origin. Специальные разрешающие заголовки CORS не добавляются. Это сохраняет маленький договор и не создаёт произвольного доступа с внешних страниц.

Если позднее появится отдельный frontend, понадобятся известные разрешённые origin, политика credentials, preflight и прежние проверки пользователя. Wildcard вместе с личными cookie не является универсальным способом «починить запрос». Устройство механизма описано в руководстве MDN по CORS.

Same-origin тоже не означает, что каждый скрипт заслуживает полномочий. Ошибка XSS может действовать внутри страницы. Именно поэтому правильный вывод, CSP, токен и право на объект остаются отдельными слоями.

Наблюдение ответа

Для Alice с корректным токеном изменение её заметки должно завершиться успехом и вернуть разрешённые поля. Без токена ожидается отказ до записи. С токеном Boris попытка изменения записи 1 всё равно должна быть недоступной.

Отдельно отправьте массив вместо объекта, лишний owner_id, число вместо title и строку сверх предела. Наблюдение касается не только кода ответа: содержимое базы должно остаться прежним при каждом отклонённом запросе.

Сохраните исходный текст заметки перед каждой попыткой и сравнивайте его после ответа. Если клиент показывает отказ, а запись всё же изменилась, сообщение об ошибке скрывает нарушение границы: проверку нужно перенести до операции записи, а затем повторить тот же сценарий.

Если API возвращает HTML-страницу ошибки, клиенту сложнее сохранить договор. В заключительном исходнике маршруты /api получают короткий JSON с error и requestId. Внутренние трассировки не передаются ни в одном формате.

Совместимость и повтор

Обновление не является автоматически безопасным для конкурентного редактирования. Два клиента могут отправить разные новые тексты и последний перезапишет первый. Для продукта с совместной работой понадобится версия объекта или другой механизм согласования.

CSRF-токен не решает этот конфликт. Он относится к происхождению действия в контексте сессии. Аналогично успешный HTTP-ответ не доказывает, что пользователь действительно хотел повторно отправить ту же операцию.

При изменении договора API сохраняйте описание обязательных полей и примеры отказов. Новое необязательное поле может быть совместимым расширением, а новое обязательное — нарушить прежний клиент. Проверка безопасности не должна скрывать обычную проблему версий.

Разбор JSON и новые поля

Обычный JSON-парсер может выбрать последнее из повторных имён поля. В финальном исходнике object_pairs_hook замечает такой повтор и отклоняет тело. Это сохраняет понятное соответствие между документом запроса и одним значением title.

Дополнительное поле owner_id также отклоняется, а не копируется в UPDATE. Полезная привычка — перечислять разрешённые изменения явно. Автоматическое применение всех ключей клиента к объекту может передать управление полем, которое не предназначено для редактора.

Даже правильное строковое значение может превышать предел. Поэтому тип и диапазон проверяются отдельно. Синтаксически корректный JSON не доказывает пригодность конкретной заметке.

Когда API изменится, сохраните версию договора и причины новых отказов. Прежний клиент может отправлять только title, тогда как наша текущая операция требует также text. Это ошибка совместимости, и её не нужно объяснять случайной «нестабильностью сети».

В контрольном сценарии сначала выполните корректный запрос своего владельца, затем изменяйте одно условие: токен, номер, тип или набор полей. Несколько нарушений одновременно покажут только первый отказ и не подтвердят остальные механизмы.

Теперь HTML и JSON проходят одну модель пользователя и объекта. Следующий урок ограничит число дорогих попыток, чтобы проверка пароля и обработка данных не оставались неограниченным ресурсом.