Безопасность 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 проходят одну модель пользователя и объекта. Следующий урок ограничит число дорогих попыток, чтобы проверка пароля и обработка данных не оставались неограниченным ресурсом.