Сегодня разбираем материал dev.to о теме «Как я создал безопасные Firebase Cloud Functions с правами администратора и ограничением частоты запросов». Практический разбор с шагами и примерами, который можно быстро применить в своей работе.
Вот как я настроил это в реальной панели администратора, а не в учебном демо.
- Проблема
- Шаг 1: Хранение ролей администраторов в Firestore
- Шаг 2: Вспомогательная функция проверки прав доступа
- Шаг 3: Ограничение частоты запросов с помощью Firestore
- Шаг 4: Собираем всё вместе
- Организация Cloud Functions
- Ошибки, которые я допустил
- Когда использовать этот паттерн
- Ответы на эти вопросы могут быть для вас полезными
Проблема
Я разработал панель администратора для одноимённого мобильного приложения, где администратору нужно отправлять push-уведомления, искать пользователей и модерировать контент. Все эти действия выполняются через Cloud Functions, которые вызываются со стороны клиента.
Проблема в том, что в вызываемых функциях Firebase нет встроенной проверки на наличие роли администратора, что позволяет любому аутентифицированному пользователю доступ к любым эндпоинтам.
Это опасно не только с точки зрения чтения или изменения данных. В административной панели обычно есть действия с высоким риском: массовые рассылки, удаление контента, пересчёт аналитики, экспорт данных. Если не поставить проверку прав на самом входе в функцию, UI может выглядеть «закрытым», но backend всё равно останется доступным для прямого вызова через SDK или подменённый клиент.
По этой теме полезно отдельно посмотреть За пределами
border-radius: что открывает CSS-свойствоcorner-shapeдля повседневного UI, чтобы расширить контекст и сравнить подходы.
Шаг 1: Хранение ролей администраторов в Firestore
Я держу простую коллекцию admins в Firestore. Каждый документ в коллекции соответствует UID пользователя и содержит его роль и список разрешений. Такая структура позволяет гибко управлять доступом без изменения кода функций — достаточно обновить запись в базе.
Шаг 2: Вспомогательная функция проверки прав доступа
Каждая административная Cloud Function вызывает её перед выполнением любых действий:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
interface AdminData { uid: string; role: string; permissions: string[];
}
async function verifyAdminPermission( context: functions.https.CallableContext, requiredPermission: string
): Promise<AdminData> { // Проверяем, аутентифицирован ли пользователь вообще if (!context.auth) { throw new functions.https.HttpsError( "unauthenticated", "You must be logged in." ); }
const uid = context.auth.uid;
// Проверяем, существует ли пользователь в коллекции admins const adminDoc = await admin .firestore() .collection("admins") .doc(uid) .get();
if (!adminDoc.exists) { throw new functions.https.HttpsError( "permission-denied", "You are not an admin." ); }
const adminData = adminDoc.data() as AdminData;
// Проверяем конкретное разрешение if ( adminData.role !== "super_admin" && !adminData.permissions.includes(requiredPermission) ) { throw new functions.https.HttpsError( "permission-denied", `You don't have the "${requiredPermission}" permission.` ); }
return { uid, ...adminData };
}
Роль super_admin обходит все проверки разрешений, в то время как другие роли требуют наличия конкретных разрешений, указанных в их документе Firestore.
Шаг 3: Ограничение частоты запросов с помощью Firestore
Я предпочёл не добавлять Redis или другие внешние сервисы для ограничения частоты запросов, так как Firestore отлично подходит для управления трафиком на уровне администраторов.
async function applyRateLimit( uid: string, action: string, maxRequests: number, windowMs: number
): Promise<void> { const now = Date.now(); const windowStart = now - windowMs;
const rateLimitRef = admin .firestore() .collection("rateLimits") .doc(`${uid}_${action}`);
const doc = await rateLimitRef.get();
if (doc.exists) { const data = doc.data()!;
// Фильтруем устаревшие временные метки за пределами окна const recentRequests = (data.timestamps as number[]).filter( (t) => t > windowStart );
if (recentRequests.length >= maxRequests) { throw new functions.https.HttpsError( "resource-exhausted", `Rate limit exceeded. Max ${maxRequests} requests per ${windowMs / 1000} seconds.` ); }
// Добавляем текущую временную метку await rateLimitRef.update({ timestamps: [...recentRequests, now], }); } else { // Первый запрос await rateLimitRef.set({ timestamps: [now], }); }
}
Это решение по ограничению частоты запросов не является самым эффективным, но для панели с небольшим числом пользователей оно работает хорошо и эффективно.
Именно здесь важно смотреть не на абстрактную «правильность» архитектуры, а на реальную нагрузку. У меня не публичный API с тысячами запросов в секунду, а админка с единичными операциями: поиск пользователей, запуск рассылки, просмотр статистики. Для такого сценария Firestore даёт нормальный баланс между простотой, стоимостью и предсказуемостью. Когда лимиты становятся частью действительно горячего контура, тогда уже имеет смысл переносить их в Redis или в отдельный edge-слой.
Шаг 4: Собираем всё вместе
Вот как выглядит реальная административная Cloud Function с обоими вспомогательными элементами. Сначала вызывается verifyAdminPermission — если пользователь не прошёл проверку, функция немедленно завершается с ошибкой. Затем applyRateLimit отсекает злоупотребления. И только после этого выполняется основная бизнес-логика.
Я также рекомендую сразу добавить журналирование административных действий: кто вызвал функцию, когда, с каким UID и каким результатом. Такой audit trail сильно упрощает разбор инцидентов и спорных действий модераторов.
Организация Cloud Functions
Когда у вас 10 и более административных функций, один файл index.ts становится нечитаемым. Я разделил их на структуру папок:
functions/ src/ admin/ notifications.ts // sendAdminNotification, estimateAudience users.ts // searchUsers, moderateUser analytics.ts // getDashboardStats index.ts // barrel-файл — реэкспортирует всё index.ts // основная точка входа — экспортирует из ./admin
Barrel-файл (файл-агрегатор) сохраняет импорты чистыми: каждый модуль отвечает за свою область, а основной index.ts просто собирает всё в одном месте.
Такое разбиение даёт ещё один практический плюс: проверки вроде verifyAdminPermission и applyRateLimit проще применять единообразно. Когда функции сгруппированы по доменам, вы быстрее замечаете, где забыли вызвать общий guard, а где лимиты должны отличаться. В реальной поддержке это важнее «красивой структуры» само по себе, потому что снижает вероятность случайной дыры в одной из новых функций.
Ошибки, которые я допустил
1. Забыл обрабатывать устаревшие FCM-токены
В первый раз, когда я отправил рассылку, 30% токенов оказались устаревшими. Firebase не очищает их за вас. Теперь я удаляю токены, которые возвращают ошибку messaging/registration-token-not-registered после каждой отправки.
2. Не использовал пакетное чтение из Firestore
Я делал отдельные вызовы getDoc() в цикле для проверки профилей пользователей. Переключился на один запрос where() — время выполнения сократилось с 3 секунд до 200 мс для 500 пользователей.
3. Документ ограничения частоты запросов рос бесконечно
Мой первый ограничитель частоты запросов добавлял временные метки, но никогда не очищал старые. Через месяц каждый документ содержал тысячи записей. Шаг фильтрации в applyRateLimit выше исправляет это — он сохраняет только временные метки в пределах текущего окна.
Когда использовать этот паттерн
Эта схема работает, когда:
- У вас небольшое количество пользователей-администраторов (менее 50)
- Вы хотите разграничение прав на основе ролей без подключения стороннего сервиса аутентификации
- Ваши потребности в ограничении частоты запросов базовые (не тысячи запросов в секунду)
Если вам нужно высоконагруженное ограничение частоты запросов — используйте Redis. Если нужен сложный RBAC (ролевое управление доступом, Role-Based Access Control) — используйте полноценного провайдера аутентификации. Но для большинства панелей администратора проверки на основе Firestore просты, бесплатны и вполне достаточны.
Ответы на эти вопросы могут быть для вас полезными
Можно ли использовать Firebase Custom Claims вместо коллекции admins в Firestore?
Да, Custom Claims — жизнеспособная альтернатива. Их преимущество в том, что они передаются прямо в токене и не требуют дополнительного чтения из Firestore при каждом запросе. Недостаток — изменения вступают в силу только после обновления токена пользователем, что может занять до часа. Коллекция admins в Firestore даёт мгновенный эффект при изменении прав.
Что произойдёт, если документ в коллекции rateLimits будет удалён вручную?
Функция applyRateLimit обрабатывает этот случай: если документ не существует, она создаёт его заново с первой временной меткой. Счётчик просто сбросится, что некритично для административной панели.
Как защитить саму коллекцию admins от несанкционированного изменения?
Правила безопасности Firestore (Firestore Security Rules) должны запрещать запись в коллекцию admins со стороны клиента полностью. Изменения вносятся только через Firebase Admin SDK — то есть через серверный код или Firebase Console вручную.
Подходит ли этот паттерн для HTTP-функций, а не только для вызываемых (Callable) функций?
Для HTTP-функций логика аналогична, но context.auth там недоступен. Вместо него нужно вручную верифицировать токен через admin.auth().verifyIdToken(), извлекая его из заголовка Authorization. Вызываемые функции делают это автоматически, поэтому для административных эндпоинтов они удобнее.
Как часто нужно чистить коллекцию rateLimits?
Сама функция applyRateLimit уже фильтрует устаревшие временные метки при каждом запросе, поэтому документы не растут бесконечно. Тем не менее документы для неактивных пользователей остаются в коллекции. Для порядка можно настроить Cloud Scheduler, который раз в неделю удаляет документы, не обновлявшиеся дольше заданного периода.


