Самое необходимое
Две оси Router на двух портах (плоскость данных8080, плоскость управления 8090), построенные на основе одного и того же общего AppState. Плоскость данных требует ключа API на любом маршруте; Для управления плоскостями требуется выделенная консоль аудитории JWT — эти два механизма никогда не пересекаются. Ограничение скорости применяется дважды: по IP-адресу до аутентификации, а затем по аутентифицированному субъекту после. Автоматический выключатель не является глобальным промежуточным программным обеспечением: это объект для каждой службы (и для каждой выделенной цели для PostgREST), вызываемый непосредственно в коде прокси. И последний прокси меняет транспорт в зависимости от маршрута — иногда в соответствии с методом HTTP или поиском в базе данных: запрос/ответ NATS для большей части трафика, прямой поток HTTP для хранения, три варианта реального времени и выделенный Postgres CRUD.
Один шлюз, две совершенно разные аудитории
Трафик плоскости данных поступает из SDK или клиентского приложения: объем анонимных запросов или запросов, аутентифицированных ключом API, с профилем злоупотреблений, близким к любому общедоступному API. Плоскость управления трафиком исходит из Studio — интерфейса администрирования проекта — и выполняет конфиденциальные операции: создание проекта, ротацию ключей, чтение журналов арендатора. У них общая цель (проксирование к одним и тем же внутренним сервисам: aura-auth, aura-db, aura-storage и т. д.), но не одинаковая поверхность риска.
Прохождение обоих через один и тот же маршрутизатор требует выбора между двумя плохими вариантами: либо Studio CORS наследует подстановочный знак, необходимый для общедоступного SDK (Access-Control-Allow-Origin: *), либо SDK наследует ограниченный список источников, предназначенный для внутренней информационной панели. Код шлюза aura устраняет это противоречие с main.rs: два отдельных Router, каждый со своим собственным CorsLayer — подстановочным знаком, разрешенным на стороне плоскости данных, отклоненным и зарегистрированным как ошибка на стороне плоскости управления.
Два маршрутизатора axum, один общий AppState
Разделение не представляет собой отдельное развертывание: два плана выполняются в одном процессе, на одном и том же AppState (пулы Postgres, клиент NATS, кэши Moka, автоматические выключатели). Отличается только конструкция Router: две специальные функции, каждая из которых вызывается один раз при запуске и обслуживается двумя отдельными TcpListener.
Два графа маршрутов начинаются с одной и той же базы service_routes(): одни и те же обработчики прокси (db_proxy, storage_proxy, functions_proxy…) монтируются в обоих планах, с дополнительными маршрутами, специфичными для каждого. Повторное использование одних и тех же обработчиков позволяет избежать двойной реализации прокси; расхождение только в промежуточном программном обеспечении позволяет избежать дублирования бизнес-логики для достижения границ безопасности. Если ваша серверная часть сама по себе структурирована как мультисервисное рабочее пространство Cargo, см. наше Руководство по архитектуре рабочего пространства Cargo — шлюз — это всего лишь один ящик среди других в этом подразделении.
Аутентификация расходится при входе
На уровне данных ключ API является обязательным на любом маршруте, за исключением нескольких действительно общедоступных путей (/health, JWKS, конечных точек регистрации). Он передается как заголовок apikey или X-API-Key или, только для маршрутов потоковой передачи WebSocket и SSE, как параметр ?apikey=. Код явно запрещает этот последний режим для ключа service_role: ключ URL-адреса просачивается в журналы доступа, трассировки OTel и заголовок Referer. JWT остается необязательным на стороне плоскости данных: без него вызывающая сторона остается anon; при этом он становится authenticated.
В плоскости управления API-ключа не существует: принимается только JWT-консоль, аудитория которой должна быть именно aurabase-control. Роль не несет в себе сам токен — она пересчитывается при каждом запросе членства пользователя в организации-владельце проекта, наследуемой через связь проект → организация.
| Требуется токен | Ключ API (apikey/X-API-Key), всегда | Консоль JWT (авторизация: носитель), всегда |
|---|---|---|
| Повышение роли | Необязательный JWT: аноним → аутентифицированный | RBAC, унаследованный от организации (владелец/администратор/разработчик/просмотрщик) |
| Введите строку запроса | Допускается только для WS/SSE, никогда для service_role. | Непригодный |
| Ожидаемая аудитория | Целевой проект (UUID пути) | исправлен «аурабаза-контроль» |
| КОРС | Подстановочный знак * разрешено | Подстановочный знак отклонен, только источники Studio |
Реальный порядок промежуточного программного обеспечения (и почему это важно)
axum объединяет промежуточное программное обеспечение с последовательными вызовами .layer() — и правило, управляющее порядком выполнения, на практике удивительно: ПОСЛЕДНИЙ размещенный .layer() становится самым НАРУЖНЫМ слоем, поэтому первый, через который проходит входящий запрос, и последний, увидевший ответ, уходит. Таким образом, линейное чтение файла дает порядок, обратный фактическому порядку выполнения.
Промежуточное программное обеспечение request_id устанавливает заголовок X-Request-Id только для ОТВЕТА, но не для входящего запроса. Поскольку AccessLogLayer размещается после него в файле (то есть более внешний и, следовательно, проходится раньше), при захвате поля request_id считывается заголовок, отправленный клиентом, а не идентификатор, сгенерированный дальше в цепочке. Если вызывающая сторона не предоставила X-Request-Id, строка журнала доступа оставляет пустое поле, а возвращаемый ответ содержит только что сгенерированный UUID. Это не скрытый дефект — напоминание о том, что порядок записи строки .layer() ничего не гарантирует относительно логического порядка, который мы ей приписываем.
Ограничение скорости: сначала IP, затем актер
Ограничение скорости применяется за два отдельных прохода в два разных момента цепочки. Первый запускается ДО аутентификации и ограничения по IP-адресу — общий фильтр защиты от флуда, активный даже на дорогах общего пользования: без него неаутентифицированный поток может забить дорогостоящую конечную точку, например агрегацию журналов, даже не запуская проверку JWT. Второй запускается ПОСЛЕ аутентификации и ограничивает субъекта — ключ API или пользователя — с использованием утверждений, которые только что были введены при аутентификации: это реальная квота продукта, которая учитывается при выставлении счетов и планировании.
Реализация использует крейт governor (корзину токенов) для локальных вычислений, сценарий Lua Redis со скользящим окном для распределения между экземплярами шлюза и локальный резервный вариант (кеш Moka), если Redis недоступен. Настройки репозитория по умолчанию: 100 запросов в секунду, пакет 1000.
Автоматический выключатель — это не слой, это объект для каждой цели
В отличие от остальной части цепочки, автоматический выключатель не появляется ни в одном .layer(). AppState содержит один экземпляр CircuitBreaker для каждой службы (аутентификация, база данных, реальное время, хранилище, функции, уведомления, искусственный интеллект, обеспечение, управление), и сам прокси-код, а не маршрутизатор, вызывает try_acquire_probe() перед попыткой запроса, а затем record_success() или record_failure() в зависимости от результата.
Случай с PostgREST отличается: проекты в выделенной топологии (Postgres и PostgREST для конкретного проекта) не имеют общего домена сбоя — каждый процесс PostgREST является своей собственной целью. Таким образом, шлюз поддерживает таблицу автоматических выключателей, индексированную по решенной цели, заполняемую на лету и очищаемую каждые 60 секунд с помощью очистки, удаляющей неактивные записи: без этой очистки каждый новый выделенный проект будет добавлять запись, которая никогда не исчезнет.
Токен проверки возвращается как Drop, если он никогда не используется явно — полезно, когда все попытки запроса истекают по тайм-ауту, не достигая ветки, которая могла бы его освободить. А автоматический повтор запускается только при строгом доказательстве недоставки на стороне NATS (NoResponders): простой тайм-аут шлюза ничего не доказывает о реальной доставке запроса, и при его воспроизведении он может быть выполнен дважды.
Последняя ссылка: NATS или прямой HTTP, никогда случайно.
Конечный прокси не говорит ни по одному протоколу в обратном порядке, и выбор не фиксируется маршрутом: он может зависеть от метода HTTP или даже от поиска в базе данных. Для большей части трафика (аутентификация, функции, уведомления, управление и большая часть базы данных) шлюз сериализует HTTP-запрос в конверте NATS и отправляет его в виде запроса/ответа субъекту, выделенному для службы. Это процедура туда и обратно без TCP-квитирования, задокументированная в коде как значительно более быстрая, чем классический HTTP-прокси для трафика этого типа RPC.
Storage, три варианта реального времени (WebSocket, SSE и REST для трансляции/каналов/присутствия) и — условно — запросы Postgres CRUD выходят из этого пути и проходят через работающий пул HTTP-клиента. Система хранения сделала этот выбор явно: кодирование двоичного тела в конверте NATS требует его сериализации, полной загрузки в память с обоих концов и соблюдения ограничения на размер сообщения NATS — реальная цена для больших объектов. WebSocket и SSE просто не допускают семантики запроса/ответа: обновление протокола и поток, который остается открытым, не имеют эквивалента NATS.
Самый интересный случай — /v1/db/*, чей обработчик принимает решение самостоятельно при каждом запросе: маршруты управления (схема, политики, необработанный SQL) всегда идут через NATS к aura-db, PUT всегда идет в NATS (PostgREST возвращает 405 при полной замене), проект MongoDB всегда идет в NATS — и только CRUD в проекте Postgres с выделенным разрешенным экземпляром PostgREST идет в Direct HTTP. Если этот выделенный экземпляр не разрешен, шлюз отвечает 503 вместо того, чтобы вернуться к общему PostgREST: предполагается закрытие при отказе, а не ухудшенный тихий резервный вариант. Заголовки безопасности (строгий CSP, без учетных данных CORS) применяются единообразно ко всем этим путям и размещаются в самом конце цепочки, прежде чем ответ покинет шлюз.
Бюджет тайм-аута для каждого маршрута, а не глобальный тайм-аут.
Шлюз применяет TimeoutLayer PER GROUP маршрутов, а не глобальный тайм-аут — выбор, связанный с той же механикой укладки, что и порядок промежуточного программного обеспечения. Маршрут функций Edge требует гораздо большего бюджета, чем остальные (функция может законно работать в течение нескольких минут): значение по умолчанию для репозитория составляет 30 секунд для большинства маршрутов по сравнению с 380 секундами для /v1/functions/*.
Если разместить один глобальный TimeoutLayer поверх всего, обе группы будут иметь одинаковый предел: всегда выигрывает самый короткий тайм-аут, помещенный в самую внешнюю позицию, независимо от более длительного тайм-аута, помещенного дальше внутри. Единственный способ предоставить функциям отдельный бюджет состоит в том, чтобы они НИКОГДА не входили в общую обертку: каждая ветвь маршрутов несет свой собственный TimeoutLayer, размещенный до слияния двух маршрутизаторов, и после этого глобальный тайм-аут не применяется.
Воспроизведите этот шаблон в другом месте: контрольный список
- Разделять по ПЛАНУ (поверхности воздействия), а не по службе: скомпрометированный общедоступный SDK никогда не должен попадать в список источников CORS на панели администратора.
- Сохраняйте одно общее состояние, а не два отдельных развертывания — дублирование бизнес-логики обходится дороже, чем дублирование маршрутизатора.
- Проверьте ФАКТИЧЕСКИЙ порядок промежуточного программного обеспечения, отслеживая его от последнего
.layer(), а не от линейного чтения файла. - Отдельное ограничение скорости на IP-адрес (до аутентификации) и квоты на актера (после) — в противном случае поток без аутентификации потребует дорогостоящей проверки без ограничений.
- Разместите прерыватель как можно ближе к фактическому сетевому вызову в прокси-сервере и определите его размер по цели, если домен сбоя не является общим.
- Воспроизводите запрос только при подтверждении недоставки, а не по простому тайм-ауту.
- Перед объединением маршрутизаторов дайте каждой группе маршрутов свой собственный бюджет таймаута, а не глобальный
TimeoutLayer, который перезапишет самый длинный бюджет.