مجتبی پاکزاد
مجتبی پاکزاد
خواندن ۱۳ دقیقه·۷ ماه پیش

دسترسی مبتنی بر نقش کاربری یا RBAC

کاور پکیج spatie/laravel-permission
کاور پکیج spatie/laravel-permission


دسترسی مبتنی بر نقش کاربری یا Role Based Access Control که به اختصار RBAC هم نامیده می‌شود مفهومی است که می‌گوید کاربران می‌توانند نقش‌های کاربری مختلفی داشته باشند که هر نقش کاربری، دسترسی‌های مختلفی دارد.


برای مثال یک کاربر دارای نقش‌های کاربری «نویسنده مقاله» و «مدیر محصولات» باشد. این کاربر دارای دو نقش کاربری است و در هر نقش، دسترسی‌ها (یا پرمیشن‌هایی) از قبیل افزودن، ویرایش، حذف و ... وجود دارند. در نتیجه این کاربر به صورت غیرمستقیم به تمامی پرمیشن‌های این دو نقش کاربری دسترسی دارد.

تفاوت بین احراز هویت (Authentication) و مجوزها (Authorization)‌ چیست؟

احراز هویت به این معنی است که کاربر هویت خود را از طریق یوزر (موبایل، ایمیل، نام کاربری یا ...) و پسورد و یا از طریق توکن (token) یا ... هویت خود را اثبات کنید. کاربران قبل از احراز هویت، مهمان نام دارند و پس از احراز هویت، کاربر احراز هویت شده هستند که ممکن است مشتری، کاربر عادی، ادمین و ... باشند.

وقتی سیستم مجوزها به هر روشی در یک اپلیکیشن (وب اپلیکیشن، موبایل اپلیکیشن و ...) پیاده‌سازی شود،‌ کاربران آن پس از احراز هویت برای دسترسی به برخی بخش‌های اپلیکیشن یا انجام برخی از قابلیت‌ها نیاز به مجوز دارند. یکی از بهترین روش‌ها برای اعطای مجوز؛ پیاده سازی RBAC است.

مزیت

این ارتباطات باعث جلوگیری از افزونگی پیاده سازی می‌شوند و اطمینان بیشتری را برای توسعه محصول در آینده به می‌دهد. به این صورت که برای افزودن پرمیشن جدید، تنها کافیست که پرمیشن را اضافه کرده و در بخش فرانت و بک اند استفاده کرد و مدیر (دارای مجوز اعطای پرمیشن)، این پرمیشن را به نقش‌های کاربری مورد نظر خود بدهد.

از کدنویسی‌های سنتی و پیچیده جلوگیری می‌شود و سیستم انعطاف پذیری بالایی دارد. مخصوصا وقتی تعداد کاربران سیستم بالا می‌رود، RBAC و کارآمد بودن آن خود را بیشتر نیز نشان می‌دهد.

نگاه دیتابیسی

معمولا ساختار دیتابیسی RBAC بسط داده می‌شود، ولی در پایه‌ترین حالت ما یک تیبل برای نگهداری مشخصات کاربران، یک تیبل برای نگهداری نقش‌های کاربری، یک تیبل برای نگهداری پرمیشن‌ها، همچنین یک تیبل واسطه (pivot)‌ برای نگهداری رابطه بین نقش‌های کاربری و دسترسی‌ها و یک تیبل واسطه برای نگهداری ارتباط بین نقش‌های کاربری و کاربران وجود دارد.

ممکن است کاربران ما در تیبل‌های مختلف نگهداری شوند و برای هر کدام بخواهیم نقش‌های کاربری مخصوص خود را داشته باشیم؛ در این حالت یکی از بهترین پیاده‌سازی‌ها را در پکیج laravel-permission توسعه داده شده توسط Spatie می‌بینیم که با استفاده از guard_name و قابلیت ارتباط پلیمورفیک این قابلیت را به ما می‌دهد که بتوانیم چندین تیبل برای نگهداری کاربران برای مثال، مشتریان، مدیران، پیک‌ها و ... داشته باشیم.

پیاده سازی

بستگی به زبان و فریمورک‌های مختلف، کدنویسی و ایجاد ارتباط متفاوت است. ما پکیج لاراول پرمیشن را در ادامه بررسی می‌کنیم. برای پیاده سازی آن کافی است یکی از پروژه‌های لاراولی خود را در IDE باز کرده و یا یک پروژه لاراول خام ایجاد کنید.

این پکیج به کاربران امکان مدیریت نقش‌های کاربری و پرمیشن‌ها در دیتابیس را می‌دهد.

نصب پکیج

composer show spatie/laravel-permission

با استفاده از دستور بالا، پکیج را نصب کنید.

افزودن تریت به مدل

این پکیج از لایه گیت لاراول برای ارائه قابلیت‌های احرازهویت استفاده کنید. مدل User یا هر مدلی که مربوط به تیبل کاربران شما است، تریت HasRoles را اضافه کنید.

توجه: دقت کنید که این مدل نباید دارای پراپرتی‌های role و roles و permission و permissions یا متدهای permissions و roles باشند، زیرا پراپرتی‌ها و یا متدهای تریت توسط مدل بازنویسی نشود.

فایل کانفیگ

این پکیج یک فایل کانفیگ با نام config/permission.php منتشر میکند که می‌توانید تنظیمات پکیج را توسط آن انجام دهید.

ساخت رول و پرمیشن

برای ساخت رول و پرمیشن می‌توان از نمونه کدهای زیر استفاده کرد. دقت کنید که اگر نیاز به افزودن ستونی مثل غیرقابل حذف بودن نقش سوپر ادمین دارید؛ مایگریشن مربوطه را ویرایش کنید. نگران نباشید نیازی به تغییر مدل Role و Permission نیست زیر این مدل‌ها از guard استفاده می‌کنند نه از fillable.

use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; $role = Role::create(['name' => 'writer']); $permission = Permission::create(['name' => 'edit articles']);

اساین کردن پرمیشن به یک نقش کاربری

با استفاده از متدهای زیر می‌توان یک پرمیشن را به یک نقش کاربری اضافه کرد:

$role->givePermissionTo($permission); $permission->assignRole($role);

سینک کردن پرمیشن‌ها برای یک نقش کاربری

برای سینک کردن چندین پرمیشن با یک نقش کاربری و برعکس، از متدهای زیر می‌توان استفاده کرد:

$role->syncPermissions($permissions); $permission->syncRoles($roles);

حذف پرمیشن از یک نقش کاربری

برای حذف یک پرمیشن از یک نقش کاربری و برعکس، از متدهای زیر می‌توان استفاده کرد:

$role->revokePermissionTo($permission); $permission->removeRole($role);

گرفتن پرمیشن‌های یک کاربر

تریت HasRoles که به مدل الکوئنتی اضافه کردیم، دسترسی مستقیم به پرمیشن‌ها را نیز فراهم می‌کند. یکی از کاربردهای گرفتن همه پرمیشن‌ها می‌تواند ساختن منوی متناسب برای هر کاربر یا نمایش برای حذف و ایجاد پرمیشن مستقیم و بدون واسطه نقش کاربری و ... است.

لیست تمام پرمیشن‌هایی که به صورت مستقیم به کاربر اساین (تخصیص داده) شده است:

// get a list of all permissions directly assigned to the user $permissionNames = $user->getPermissionNames(); // collection of name strings $permissions = $user->permissions; // collection of permission objects

لیست تمام پرمیشن‌هایی که به صورت مستقیم، غیرمستقیم و یا هر دو به کاربر اساین شده است:

// get all permissions for the user, either directly, or from roles, or from both $permissions = $user->getDirectPermissions(); $permissions = $user->getPermissionsViaRoles(); $permissions = $user->getAllPermissions();

نام نقش‌های کاربری اساین شده به کاربر

// get the names of the user's roles $roles = $user->getRoleNames(); // Returns a collection

اسکوپ‌ها

تریتی که به مدل اضافه کردیم دو اسکوپ withoutRole و withoutRole نیز به مدل اضافه می‌کند.

به ترتیب کاربرانی که دارای نقش writer هستند و کاربرانی که دارای نقش کاربری editor نیستند:

$users = User::role('writer')->get(); // Returns only users with the role 'writer' $nonEditors = User::withoutRole('editor')->get(); // Returns only users without the role 'editor'

به ترتیب کاربرانی که پرمیشن edit articles دارند و کاربرانی این پرمیشن را ندارند:

$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly) $usersWhoCannotEditArticles = User::withoutPermission('edit articles')->get(); // Returns all users without the permission 'edit articles' (inherited or directly)

فراخوانی با قدرت الوکوئنت

از آنجایی که رول‌ها (نقش‌های کاربری) و پرمیشن‌ها مدل‌هایی هستند که از مدل‌های الوکوئنت اکستند شده‌اند، کانونشن‌ها و کاندیشن‌های الوکوئنت در آنها نیز صدق می‌کنند:

$all_users_with_all_their_roles = User::with('roles')->get(); $all_users_with_all_their_direct_permissions = User::with('permissions')->get(); $all_roles_in_database = Role::all()->pluck('name'); $users_without_any_roles = User::doesntHave('roles')->get(); $all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get();

تعداد کاربران با نقش کاربری خاص

یک راه که برای شمردن تعداد کاربران با یک نقش کاربری خاص وجود دارد، استفاده از متد filter بر روی کاربران است:

$superAdminCount = User::with('roles')->get()->filter( fn ($user) => $user->roles->where('name', 'Super Admin')->toArray() )->count();

اساین کردن مستقیم پرمیشن به کاربران

افزودن پرمیشن مشخص به کاربر

$user->givePermissionTo('edit articles'); // You can also give multiple permission at once $user->givePermissionTo('edit articles', 'delete articles'); // You may also pass an array $user->givePermissionTo(['edit articles', 'delete articles']);

حذف پرمیشن از کاربر

$user->revokePermissionTo('edit articles');

سینک (افزودن/حذف) پرمیشن‌ها برای کاربر

$user->hasPermissionTo('edit articles');

بررسی اینکه کاربر پرمیشن خاصی را دارد به وسیله id

$user->hasPermissionTo('1'); $user->hasPermissionTo(Permission::find(1)->id); $user->hasPermissionTo($somePermission->id);

بررسی اینکه کاربر پرمیشن خاصی را دارد به وسیله نام پرمیشن

$user->hasAnyPermission(['edit articles', 'publish articles', 'unpublish articles']);

بررسی اینکه کاربر تمامی پرمیشن‌های مشخص شده را دارد

$user->hasAllPermissions(['edit articles', 'publish articles', 'unpublish articles']);

برای بررسی امکان پاس دادن id پرمیشن نیز وجود دارد

$user->hasAnyPermission(['edit articles', 1, 5]);

با استفاده از متد پیش فرض لاراولی can می‌توانید چک کنید که آیا یک کاربر پرمیشن خاصی را دارد:

$user->can('edit articles');

استفاده از پرمیشن‌ها به وسیله نقش‌های کاربری

اساین کردن نقش کاربری به کاربر

$user->assignRole('writer'); // You can also assign multiple roles at once $user->assignRole('writer', 'admin'); // or as an array $user->assignRole(['writer', 'admin']);

حذف نقش کاربری از کاربر

$user->removeRole('writer');

سینک نقش کاربری برای کاربر

// All current roles will be removed from the user and replaced by the array given $user->syncRoles(['writer', 'admin']);

بررسی نقش‌های کاربری

آیا یک کاربر دارای نقش کاربری خاصی است:

$user->hasRole('writer'); // or at least one role from an array of roles: $user->hasRole(['editor', 'moderator']);

آیا کاربر یکی از نقش‌های کاربری مشخص شده را دارد:

$user->hasAnyRole(['writer', 'reader']); // or $user->hasAnyRole('writer', 'reader');

آیا یک کاربر دارای تمامی نقش‌های مشخص شده است:

$user->hasAllRoles(Role::all());

آیا یک کاربر دقیقا و فقط و فقط نقش‌های مشخص شده را دارا است:

$user->hasExactRoles(Role::all());

اساین کردن پرمیشن به نقش‌های کاربری

اساین کردن پرمیشن به نقش کاربری:

$role->givePermissionTo('edit articles');

آیا نقش کاربری دارای پرمیشن مشخص است:

$role->hasPermissionTo('edit articles');

حذف پرمیشن از نقش کاربری:

$role->revokePermissionTo('edit articles');

سینک کردن پرمیشن‌ها با نقش‌های کاربری

$role->syncPermissions(['edit articles', 'delete articles']);

یک نقش کاربری چه پرمیشن‌هایی دارد؟

// get collection $role->permissions; // return only the permission names: $role->permissions->pluck('name'); // count the number of permissions assigned to a role count($role->permissions); // or $role->permissions->count();

اساین کردن پرمیشن به صورت مستقیم به کاربر

$role = Role::findByName('writer'); $role->givePermissionTo('edit articles'); $user->assignRole('writer'); $user->givePermissionTo('delete articles');

آیا یک کاربر پرمیشن‌های مشخص شده را دارد:

// Check if the user has Direct permission $user->hasDirectPermission('edit articles') // Check if the user has All direct permissions $user->hasAllDirectPermissions(['edit articles', 'delete articles']); // Check if the user has Any permission directly $user->hasAnyDirectPermission(['create articles', 'delete articles']);

دایرکتیوهای بلید

ساده‌ترین راه استفاده از دایرکتیو can در است، راه جایگزین دیگر استفاده از متد can برای کاربر است:

@can('edit articles')
//
@endcan

یا

@if(auth()->user()->can('edit articles') && $some_other_condition)
//
@endif

همچنین استفاده از دایرکتیوهای cannot و canany و guest نیز امکان‌پذیر است.

پارامتر دوم دایرکتیو can مشخص می‌کند که پرمیشن فقط برای گارد مشخصی بررسی شود. (گارد را در ابتدای مقاله توضیح دادم.)

دایرکتیو اختصاصی پکیج نیز haspermission است که دایرکتیو متناظر آن endhaspermission است.

توجه: توصیه اکید می‌کنم که صرفا دسترسی کاربران به بخش‌های مختلف اپلیکیشن را با پرمیشن‌ها بدهید و رول‌ها را مبنای اعطای حق دسترسی به بخش‌های مختلف قرار ندهید.

اگر مجبور به استفاده شدید می‌توانید از دایرکتیوها و متدهای زیر استفاده کنید.

آیا کاربر دارای نقش کاربری مشخصی است:

@role('writer') I am a writer! @else I am not a writer... @endrole

به صورت مشابه:

@hasrole('writer') I am a writer! @else I am not a writer... @endhasrole

با دایرکتیو پیش فرض لاراول

@if(auth()->user()->hasRole('writer')) // @endif

بررسی اینکه یک کاربر یک یا همه نقش‌های کاربری مشخص شده را دارد:

@hasanyrole($collectionOfRoles) I have one or more of these roles! @else I have none of these roles... @endhasanyrole // or @hasanyrole('writer|admin') I am either a writer or an admin or both! @else I have none of these roles... @endhasanyrole

آیا کاربر تمامی نقش‌های کاربری مشخص شده را دارد:

@hasallroles($collectionOfRoles) I have all of these roles! @else I do not have all of these roles... @endhasallroles // or @hasallroles('writer|admin') I am both a writer and an admin! @else I do not have all of these roles... @endhasallroles

آیا کاربر نقش کاربری مشخص شده را ندارد:

@unlessrole('does not have this role') I do not have the role @else I do have the role @endunlessrole

آیا یک کاربر دقیقا و فقط و فقط تمامی نقش‌های کاربری مشخص شده را دارد:

@hasexactroles('writer|admin') I am both a writer and an admin and nothing else! @else I do not have all of these roles or have more other roles... @endhasexactroles

کامندهای آرتیزان

ایجاد نقش کاربری و پرمیشن با استفاده از کامندهای آرتیزان:

php artisan permission:create-role writer php artisan permission:create-permission writer

اگر نام نقش کاربری بیش از یک کلمه باشد در کوتیشن قرار می‌گیرد. همچنین اگر بخواهیم این نقش کاربری در گارد مشخصی ایجاد شود، نام گارد را به عنوان پارامتر دوم به این کامندها پاس می‌دهیم:

php artisan permission:create-permission &quotedit articles&quot php artisan permission:create-role writer web php artisan permission:create-permission &quotedit articles&quot web

برای ساخت همزمان چندین نقش کاربری:

php artisan permission:create-role writer web &quotcreate articles|edit articles&quot

نمایش پرمیشن‌ها در ترمینال

php artisan permission:show

ریست کردن کش

اگر می‌خواهید کش پرمیشن‌ها را به صورت دستی پاک کنید از کامندهای زیر استفاده کنید:

php artisan permission:cache-reset

میدلور

میدلور پیش فرض

Route::group(['middleware' => ['can:publish articles']], function () { ... }); // or with static method (requires Laravel 10.9+) Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { ... });

میدلورهای پکیج

در لاراول ۱۱ فایل bootstrap/app.php را باز کرده و میدلورهای پکیج را رجیستر کنید:

->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, ]); })

در لاراول ۹ و ۱۰ می‌توانید میدلورها را در فایل app/Http/Kernel.php اضافه کنید:

// Laravel 9 uses $routeMiddleware = [ //protected $routeMiddleware = [ // Laravel 10+ uses $middlewareAliases = [ protected $middlewareAliases = [ // ... 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, ];

استفاده از میدلورها در روت‌ها

Route::group(['middleware' => ['role:manager']], function () { ... }); Route::group(['middleware' => ['permission:publish articles']], function () { ... }); Route::group(['middleware' => ['role_or_permission:publish articles']], function () { ... }); // for a specific guard: Route::group(['middleware' => ['role:manager,api']], function () { ... }); // multiple middleware Route::group(['middleware' => ['role:manager','permission:publish articles']], function () { ... });

برای چندین رول یا پرمیشن از علامت پایپ | با مفهوم OR استفاده کنید.

Route::group(['middleware' => ['role:manager|writer']], function () { ... }); Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { ... }); Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { ... }); // for a specific guard Route::group(['middleware' => ['permission:publish articles|edit articles,api']], function () { ... });

کنترلرها

در لاراول ۱۱، اگر کنترلرتان اینترفیس را ایمپلمنت می‌کند، می‌توانید میدلورهای کنترلر را با استفاده از متد رجیستر کنید:

public static function middleware(): array { return [ // examples with aliases, pipe-separated names, guards, etc: 'role_or_permission:manager|edit articles', new Middleware('role:author', only: ['index']), new Middleware(\Spatie\Permission\Middleware\RoleMiddleware::using('manager'), except:['show']), new Middleware(\Spatie\Permission\Middleware\PermissionMiddleware::using('delete records,api'), only:['destroy']), ]; }

در لاراول ۱۰ و قبلتر، میدلورها را می‌توانید در کانستراکتور رجیستر کنید:

public function __construct() { // examples: $this->middleware(['role:manager','permission:publish articles|edit articles']); $this->middleware(['role_or_permission:manager|edit articles']); // or with specific guard $this->middleware(['role_or_permission:manager|edit articles,api']); }

نقش‌های کاربری در مقابل پرمیشن‌ها

رول‌ها یا نقش‌های کاربری برای گروه‌بندی کاربران دارای مجموعه‌ای از پرمیشن‌ها تعریف می‌شوند. بنابراین همانطور که قبلتر گفتم بهتر است که پرمیشن‌ها به رول‌ها و رول‌ها به کاربران اساین شوند.

سید کردن دیتابیس

در ابتدای راه اندازی پروژه، معمولا پرمیشن‌ها، رول‌ها، کاربران، تنظیمات و ... را به صورت سید در پروژه تعریف می‌کنند. برای جلوگیری از ارورهای کانفلیتی بخاطر کش، بهتر است قبل از اجرای سیدها که منجر به ایجاد رول‌ها و پرمیشن‌های جدید می‌شود، کش آنها را پاک کنیم:

// reset cached roles and permissions app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

در زیر یک نمونه ساده از فایل سیدر دیتابیس برای ایجاد پرمیشن‌ها و رول‌های پیش فرض سیستم را می‌بینید:

use Illuminate\Database\Seeder; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; class RolesAndPermissionsSeeder extends Seeder { public function run(): void { // Reset cached roles and permissions app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); // create permissions Permission::create(['name' => 'edit articles']); Permission::create(['name' => 'delete articles']); Permission::create(['name' => 'publish articles']); Permission::create(['name' => 'unpublish articles']); // create roles and assign created permissions // this can be done as separate statements $role = Role::create(['name' => 'writer']); $role->givePermissionTo('edit articles'); // or may be done by chaining $role = Role::create(['name' => 'moderator']) ->givePermissionTo(['publish articles', 'unpublish articles']); $role = Role::create(['name' => 'super-admin']); $role->givePermissionTo(Permission::all()); } }

کش

نکته: هرگاه تغییری در پرمیشن‌‌ها و یا رول‌ها ایجاد شود که این تغییر می‌تواند شامل ایجاد، حذف و اضافه آنها شود؛ کش به صورت اتوماتیک پاک می‌شود. دقت کنید که این تغییرات باید حتما از طریق کامندها و یا متدهای خود پکیج انجام گیرد.

برای مقال متدهای زیر موجب ریست شدن کش می‌شوند:

// When handling permissions assigned to roles: $role->givePermissionTo('edit articles'); $role->revokePermissionTo('edit articles'); $role->syncPermissions(params); // When linking roles to permissions: $permission->assignRole('writer'); $permission->removeRole('writer'); $permission->syncRoles(params);

این پکیج دارای امکانات بسیار زیادی است که پرکاربردترین امکانات آن را در این مقاله به صورت خلاصه نوشتم. برای مطالعه امکانات کامل آن توصیه می‌کنم که داکیومنت آن را مطالعه نمایید.

بعضی از پست‌هام که ممکنه دوست داشته باشید:



نقش کاربریاحراز هویتلاراولlaravel permissionspatie
به عنوان توسعه دهنده در صبا ایده (آپارات، فیلیمو و ...) مشغول به کارم و در باورژن آموزش می‌دهم. حل مساله و چالش رو خیلی دوست دارم و رابطه خیلی خوبی با ریاضیات، برنامه‌نویسی و اقتصاد دارم.
شاید از این پست‌ها خوشتان بیاید