Допустим, Alice входит в рабочую область alpha. Она запрашивает /workspaces/alpha/documents/doc-b/ и получает документ из beta. Сервер распознал Alice и проверил её участие. Обе проверки прошли. Где запрос пошёл не так?
Учебный пример использует вымышленные документы и личности. Нужен Node.js 22 или новее; метод чтения применим и к другим языкам и фреймворкам. Полезный вопрос конкретен: какую область проверили и какая ограничила поиск документа?
Начните с запроса и его правила
Запишите вместе метод, путь, личность и ожидаемый результат:
GET /workspaces/alpha/documents/doc-b/
Demo identity: alice, a member of alpha only
Stored document: doc-b belongs to beta
Expected: 404, with no document in the response
Broken result: 200, containing Beta release notes
Политика примера даёт участникам доступ ко всем документам их области, без отдельных общих документов и исключений администратора. Для участника alpha документ вне alpha даёт тот же статус и тело, что отсутствующий. Запрос к области, куда пользователь не входит, получает 403 до вызова хранилища.
Нужно проследить две связи: аутентифицированный пользователь входит в область URL, а возвращённый документ относится к той же области. Успешная первая проверка оставляет вторую поиску данных.
В реальном отчёте запишите сборку или версию исходников. Маршрут рабочей копии должен совпадать с кодом обработки запроса. Для локального воспроизведения используйте синтетическую фикстуру.
Найдите регистрацию маршрута до обработчика
Скачайте request-flow.mjs и request-flow.test.mjs в одну папку. Ищите характерные слова пути, например workspaces и documents, текстовым поиском редактора. Если есть ripgrep, команды сузят поиск:
rg -n 'workspaces|documents' request-flow.mjs
rg -n 'middleware|runMiddleware|requireWorkspaceMember|findById|findInWorkspace' request-flow.mjs
Откройте createApp. Объект маршрута содержит рядом метод, шаблон пути, middleware и выбранный обработчик:
const route = {
method: 'GET',
pattern: /^\/workspaces\/([a-z0-9-]+)\/documents\/([a-z0-9-]+)\/$/,
middleware: [authenticate, requireWorkspaceMember],
handler: lookup === 'unscoped' ? getDocumentUnsafe : getDocument,
};
Прочитайте следующие строки. handle сопоставляет new URL(request.url).pathname, проверяет метод и присваивает захваченные сегменты workspaceId и documentId. Параметры query не задают эти значения. Пример принимает строчные буквы, цифры и дефисы и требует конечный слеш; это не универсальный роутер.
Во фреймворке путь может состоять из родительского mount вроде /workspaces/:workspaceId и дочернего маршрута /documents/:documentId. Прочитайте mount и передачу параметров родителя. Совпадение в обработчике не показывает оборачивающие вызов middleware.
Читайте middleware в реальном порядке
runMiddleware оборачивает обработчик через reduceRight. Для [authenticate, requireWorkspaceMember] сначала идёт аутентификация, затем участие, затем обработчик, если каждый этап вызывает next(). Значения return next() возвращаются в handle, чей catch превращает брошенный HttpError в ответ.
Request → path and method match → authenticate
→ requireWorkspaceMember → document lookup → JSON Response
↘ thrown error → handle's catch → error Response
authenticate ищет Authorization в фиксированной map и помещает результат в ctx.user. demo-alice и demo-bob — публичные входы фикстуры, заменяющие проверенную сессию. Этого достаточно для происхождения личности в примере; реальную проверку сессии или токена нужно изучать отдельно.
Теперь откройте middleware участия:
async function requireWorkspaceMember(ctx, next) {
const { workspaceId } = ctx.params;
const allowed = ctx.user.workspaceIds.includes(workspaceId);
ctx.trace.push(`membership:${workspaceId}:${allowed ? 'allowed' : 'denied'}`);
if (!allowed) throw new HttpError(403, 'Forbidden');
return next();
}
Для Alice и пути с alpha значение allowed истинно. Функция не проверяет documentId и не загружает документ. Запишите точно: «Alice может войти в маршрут alpha». Одно «авторизована» легко приписывает проверке лишний смысл.
Отметьте остановки. Отсутствующая или неизвестная личность даёт 401 до проверки участия; неучастник получает 403 до обработчика. Если middleware вызывает next() до решения, обработчик уже мог выполниться. Место вызова не менее важно, чем имя.
Разделяйте пользователя, запрошенную область и сохранённый документ
Полезная заметка для ошибочного запроса помещается в четыре строки:
| Значение | Источник | Значение запроса | Куда проследить |
|---|---|---|---|
| Пользователь и участие | authenticate читает map личностей |
Alice, ['alpha'] |
requireWorkspaceMember |
| Запрошенная область | Первый захват маршрута | alpha |
Проверка участия, затем аргументы поиска |
| Запрошенный документ | Второй захват маршрута | doc-b |
Поиск документа |
| Фактическая область документа | Сохранённая запись | beta |
Условие поиска и ответ |
Добавляйте файлы и строки при изучении каждого источника и потребителя. В большом приложении это могут быть разные типы и пакеты, но разделение полезно: URL выражает запрос, личность — участие, запись — принадлежность.
Оба ID приходят вместе. Теперь проверьте, выходят ли они из обработчика вместе.
Откройте поиск, даже если обработчик выглядит обычным
Намеренно небезопасный обработчик:
async function getDocumentUnsafe(ctx) {
ctx.trace.push(`lookup:unscoped:${ctx.params.documentId}`);
const document = await ctx.store.findById(ctx.params.documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
В хранилище попадает только documentId. Проследите findById в createStore: условие — document.id === documentId. Нет сравнения области или поздней проверки принадлежности до сериализации. Middleware принял alpha, а поиск может вернуть запись из любой области.
Не нужны гонка или поддельная сессия. Alice сохраняет свою личность и разрешённый путь alpha, передавая ID doc-b из фикстуры. Приложение проверяет одну область ресурса, а читает другую.
В реальном проекте имя функции — слабое доказательство. Метод может внутри добавлять фильтр арендатора, наследовать ограниченную сессию базы или зависеть от правил доступа к строкам. Прочитайте реализацию и конфигурацию, прежде чем считать отсутствие аргумента неограниченным запросом. Здесь условие в памяти — весь поиск, его можно проверить напрямую.
Прогоните контрпример через всю цепочку обработчиков
В папке с обоими файлами выполните:
node request-flow.mjs
node --test request-flow.test.mjs
Установка, аккаунт и сервис не нужны. makeRequest создаёт Web Request, тесты передают его в handle, возвращающий Web Response. URL example.invalid разбирается только в памяти. Пример не вызывает fetch и не открывает слушающий сокет.
Демонстрация печатает ответы, а под каждым — пройденные этапы и вызовы хранилища:
normal: 200 {"id":"doc-a","workspaceId":"alpha","title":"Alpha release notes"}
reproduce: 200 {"id":"doc-b","workspaceId":"beta","title":"Beta release notes"}
fixed: 404 {"error":"Not found"}
Для ошибочного запроса этапы такие:
request:GET -> route:alpha:doc-b -> authenticate:alice -> membership:alpha:allowed -> lookup:unscoped:doc-b -> response:200
Массив трассировки добавлен в фикстуру и находится вне ответа. Тест также проверяет возвращённый документ и вызов findById('doc-b'). Утверждения связывают последовательность веток с фактически полученными данными.
Тест reproduce: проходит, когда небезопасная версия возвращает документ beta. Зелёный результат подтверждает воспроизведение дефекта. Отдельный тест требует 404 от исправленной версии на тот же запрос.
Передайте проверенную область в запрос данных
Исправленный обработчик сохраняет оба значения маршрута:
async function getDocument(ctx) {
const { workspaceId, documentId } = ctx.params;
ctx.trace.push(`lookup:scoped:${workspaceId}:${documentId}`);
const document = await ctx.store.findInWorkspace(workspaceId, documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
В findInWorkspace условие требует и document.workspaceId === workspaceId, и document.id === documentId. Middleware уже проверил тот же workspaceId. Фикстура не меняет ctx.params между этапами; документы и участие остаются фиксированными в запуске.
Для /workspaces/alpha/documents/doc-b/ совпадения больше нет. Отсутствующий и чужой ID дают 404 с {"error":"Not found"}. Одинаковые статус и тело не раскрывают явно случай; тесты ничего не утверждают о времени и других побочных каналах.
Проверка document.workspaceId после глобального чтения тоже могла бы остановить конкретную утечку ответа. Ограниченный поиск явно задаёт границу в вызове хранилища и не возвращает чужую запись обработчику. В памяти Array.find всё ещё просматривает записи для условия. Записанные вызовы показывают аргументы, а не ввод-вывод базы или физическую изоляцию.
При переносе на SQL или ORM изучите сформированное условие и связывание параметров и тестируйте реальный слой данных. Отзыв участия, перемещение и общий доступ документов и другие маршруты требуют своих правил и проверок.
Проверьте успех и обязательные остановки
В наборе десять тестов: одно намеренное воспроизведение и девять проверок исправленного приложения. Некоторые охватывают несколько входов:
| Запрос или условие | Ожидаемый ответ | Поведение хранилища |
|---|---|---|
Alice читает alpha/doc-a; Bob — beta/doc-b |
200, верный документ |
Оба ID доходят до findInWorkspace |
Alice читает alpha/doc-b |
404, без документа |
Поиск ограничен alpha |
Alice читает alpha/missing |
Те же статус и тело, что выше | Совпадения нет |
| Личность отсутствует или неизвестна | 401 |
Нет вызова хранилища; участие не проверяется |
Alice читает beta/doc-b |
403 |
Нет вызова хранилища |
Alice читает alpha/doc-b?workspaceId=beta |
404 |
Параметр не заменяет область маршрута |
| Неизвестный путь; неверный метод известного пути | 404; 405 с Allow: GET |
Нет вызова хранилища |
| Неожиданная ошибка хранилища | 500, общее тело ошибки |
Ошибка доходит до обёртки ответа |
Короткие пути означают /workspaces/{workspaceId}/documents/{documentId}/, с query после конечного слеша. Два успешных чтения не допускают «исправления», отклоняющего всё. Проверки отсутствия вызовов ловят отказ лишь после доступа к хранилищу. Вместе они проверяют больше финальных статусов.
Тесты охватывают внутрипроцессную обработку, порядок middleware, аргументы поиска и JSON-ответы маршрута. Не проверяют HTTP-транспорт, reverse proxy, реальную проверку личности и базу. В приложении начните с фактического HTTP-адаптера и проверьте выбор изученных обработчика и middleware.
Оставьте проверяемую заметку
Опишите результат через запрос и точные места использования значений:
Trigger: Alice + GET /workspaces/alpha/documents/doc-b/
Route: createApp captures alpha and doc-b.
Membership: requireWorkspaceMember allows Alice for alpha.
Old read: getDocumentUnsafe passes only doc-b to findById.
Observed: 200 returns a row whose workspaceId is beta.
Fix: getDocument passes alpha and doc-b to findInWorkspace.
Regression: same request returns 404; both valid member reads still return 200.
Scope: in-memory request pipeline; production adapter and storage are untested.
Добавьте места в исходниках и тестовую команду. Если коллега спросит «Разве маршрут уже не авторизован?», заметка покажет, что именно проверялось и где область перестала использоваться.