Каждому SaaS нужны роли. По учебнику это выглядит так: таблица ролей, таблица прав, две промежуточные таблицы, сидер для каждой и класс политик — и всё это до написания хоть одной фичи. Для инди-приложения с двумя ролями (админ и пользователь) я пошел другим путем: одна строковая колонка и минорная мидлваря на 15 строк. Вот как это выглядит целиком и почему этого достаточно.
Колонка
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default('user')->after('email');
});
Одна строка со значением по умолчанию 'user' . role находится в массиве модели $fillable , поэтому передается через User::create() и ->update() , как и любой другой атрибут. Здесь нет таблицы ролей, потому что при таком масштабе она ничего не дает: роли здесь — это фиксированный закрытый набор, определяемый разработчиком, а не данные, которыми управляет клиент.
Правило, которым я руководствуюсь: если набор ролей выбирается вами и меняется при деплое, это колонка. Если клиент создает и назначает роли во время работы приложения, это таблица. Большинство стартовых наборов и MVP — это первый вариант, одетый в архитектуру второго.
Мидлварь
class EnsureUserHasRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
$user = $request->user();
if (! $user || ! in_array($user->role, $roles, true)) {
return response()->json(['message' => 'Forbidden'], 403);
}
return $next($request);
}
}
Стоит обратить внимание на три вещи:
- Вариативный $roles . Зарегистрированный как алиас role в bootstrap/app.php , сейчас он используется как role:admin — но завтра role:admin,editor будет работать без изменения кода. Мидлварь не знает названий ваших ролей; она просто проверяет вхождение.
- Строгое in_array . Этот третий аргумент, true , отключает приведение типов в PHP. На границе авторизации строгость — это не педантичность: нестрогое сравнение — это как раз то, из-за чего проскакивают неожиданные совпадения.
- 403, а не 404. Валидный токен с неправильной ролью получает {"message": "Forbidden"} и ошибку 403. Я не скрываю существование эндпоинта — безопасность через неясность в вашем собственном админском API ничего не дает, но превращает отладку в кошмар.
В определении роутов важен порядок:
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
Route::apiResource('users', UserController::class);
});
auth:sanctum выполняется первым, поэтому к моменту запуска проверки роли $request->user() уже заполнена. Поменяйте порядок местами, и мидлварь будет возвращать 403 для каждого запроса, включая запросы валидных админов, — отличный баг для полуночной отладки.
В чем все ошибаются: кто назначает роли
Колонка ролей безопасна только тогда, когда безопасен путь назначения. У меня есть три правила:
- Регистрация жестко задает роль. Обработчик регистрации записывает 'role' => 'user' напрямую; запрос никогда не содержит поле роли. Поле role в запросе на регистрацию было бы формой повышения привилегий с дополнительными шагами.
- Сидер отвечает за админа. User::firstOrCreate(['email' => 'admin@demo.io'], [..., 'role' => 'admin']) — идиопатический (идемпотентный), поэтому его можно запускать при каждом деплое без дублирования данных. Первый админ существует потому, что его создал деплой, а не потому, что это позволила какая-то форма.
- Изменение ролей проходит через эндпоинт только для админов. StoreUserRequest разрешает role: in:admin,user , и это нормально — весь ресурс users находится за role:admin . Когда админ повышает пользователя — это фича; когда незнакомец повышает сам себя — это уязвимость. Поле то же самое, граница доверия другая.
Когда от этого нужно отказаться
Я добавлю настоящую систему ролей и прав в тот день, когда клиенту понадобится определять собственные роли: роли для каждого тенанта, гранулярные права вроде users.delete . До тех пор колонка представляет собой одну строку, мидлварь состоит из 15 строк, и есть ровно одно место, где дается ответ на вопрос «разрешено ли этому пользователю?».
Простая авторизация, в которой полностью разобрались, лучше гибкой авторизации, которую никто не проверял. Скучные решения снова побеждают.
Комментарии (0)
Пока нет комментариев — будьте первым.