در این بخش خواهیم دید که چگونه می توانیم سیاست های مربوط به authorization را در دیتابیس اجرا کنیم. تصور کنید که یک برنامه مدیریت ارتباط با مشتری (CRM) مانند زیر داریم که لیستی از مشتری ها و نماینده فروش نشان می دهد.
دقت کنید که در این لیست کاربری به نام Ted Bossman ادمین اصلی برنامه است.
اکنون به مایگریشن ها نگاهی می اندازیم. ابتدا به سراغ مایگریشن مربوط به جدول کاربر ها می رویم.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->boolean('is_owner')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
همانطور که می بینید یک ستون از نوع Boolean داریم که مشخص می کند آیا کاربر ادمین هست یا نه.
حال به مایگریشن مشتری ها نگاه می کنیم.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCustomersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->foreignId('sales_rep_id')->constrained('users');
$table->string('name');
$table->string('city');
$table->string('state');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('customers');
}
}
در اینجا ستونی با نام sales_rep_id
داریم که مشتری را به کاربر وصل می کند.
تصور کنید نیازمندی جدیدی مطرح شده است که مسئول های فروش تنها می توانند مشتری های خودشان را ببیند. البته ادمین برنامه یا همان Ted Bossman اجازه دارد همه مشتری ها را ببیند. چگونه این قابلیت را پیاده سازی می کنیم؟
کاری که از ما خواسته شده است به وظیفه policyها شباهت دارد.
با اجرای دستور زیر یک کلاس policy
برای مشتری ها ایجاد می کنیم.
php artisan make:policy CustomerPolicy
یک متد با نام view
می سازیم که سیاست نمایش مشتری را در آن پیاده سازی کنیم. ورودی این متد کاربر لاگین شده و مشتری ای است که می خواهیم نمایش دهیم.
<?php
namespace App\Policies;
use App\Customer;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CustomerPolicy
{
use HandlesAuthorization;
public function view(User $user, Customer $customer)
{
return $user->is_owner || $user->id === $customer->sales_rep_id;
}
}
می دانیم که اگر کاربر ادمین است می تواند همه مشتری ها را ببیند. پس بررسی این شرط را ابتدای متد انجام می دهیم. سایر کاربر ها تنها در صورتی می توانند مشتری را ببیند که مسئول فروش آن باشند.
پیش از اینکه بتوانیم سیاست را تست کنیم باید کاربری را در برنامه لاگین کنیم. برای اینکه کل قابلیت لاگین را به برنامه اضافه نکنیم به صورت دستی کاربری که می خواهیم را لاگین می کنیم.
این کار با استفاده از تابع login
در کلاس Auth
انجام می شود.
Auth::login(User::where('name', 'Sarah Seller')->first());
اکنون که کاربر لاگین شده است چگونه سیاست مان را بررسی کنیم؟ این کار را می توان در فایل blade ای که مشتری را نشان دهد انجام داد. (به can@ توجه کنید.)
@foreach ($customers as $customer)
@can('view', $customer)
<tr class="bg-white">
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 font-medium text-gray-900">
{{ $customer->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
{{ $customer->city }}, {{ $customer->state }}
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
<div class="flex items-center">
<div>{{ $customer->salesRep->name }}</div>
@if ($customer->salesRep->is_owner)
<div
class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Owner
</div>
@endif
</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap text-right border-b border-gray-200 text-sm leading-5 font-medium">
<a href="#"
class="text-indigo-600 hover:text-indigo-900 focus:outline-none focus:underline">Edit</a>
</td>
</tr>
@endcan
@endforeach
اکنون اگر صفحه را دوباره بارگذاری کنیم، تنها مشتری های کاربر لاگین شده را می بینیم. اما به سرعت متوجه مشکلی می شویم. ما 75 نتیجه گرفتیم و از نتیجه 1 تا 15 را می بینیم اما تنها 6 نتیجه در صفحه وجود دارد.
دلیل آن هم این است که در این صفحه تنها 6 ردیف، مشتری های کاربر لاگین شده هستند. اگر به صفحه دوم برویم همین مشکل را می بینیم. باید 15 نتیجه داشته باشیم اما نداریم. از آن جایی که این روش صفحه بندی را خراب می کند، راه حل خوبی نیست. چه کار دیگری می توانیم انجام دهیم؟
ابتدا تغییری که در فایل blade ایجاد کردیم را بر می گردانیم. حال به کنترلر مشتری ها بر می گردیم و از آن جایی که نمی توانیم صفحه بندی داشته باشیم همه نتیجه ها را از دیتابیس می خوانیم و آن ها را به طور دستی فیلتر می کنیم.
<?php
namespace App\Http\Controllers;
use App\Customer;
use App\User;
use Illuminate\Support\Facades\Auth;
class CustomersController extends Controller
{
public function index()
{
Auth::login(User::where('name', 'Sarah Seller')->first());
$customers = Customer::query()
->with('salesRep')
->orderBy('name')
->get()
->filter(function ($customer) {
return Auth::user()->can('view', $customer);
});
return view('customers', ['customers' => $customers]);
}
}
حال که صفحه بندی را حذف کردیم باید در فایل blade هم کد های مربوط به نمایش لینک ها را حذف کنیم.
{{-- {{ $customers->links() }}--}}
اگر صفحه را دوباره بارگذاری کنیم تقریبا نتیجه ای که می خواستیم را می بینیم. این صفحه تنها مشتری های کاربر لاگین شده را نشان می دهد اما صفحه بندی را از دست دادیم و از آن بدتر ما به جای اینکه تنها مشتری های کاربر وارد شده را از دیتابیس بگیریم، همه مشتری ها دریافت می کنیم.
اگر برنامه ما هزاران مشتری داشته باشد، این یک ایراد بزرگ در بهره وری محسوب می شود.
به همین دلیل بهتر است که این کار را در لایه دیتابیس انجام دهیم.
لینک های صفحه بندی را دوباره فعال می کنیم و به کنترلر مشتری ها بر می گردیم. کد را به حالت قبلی بر می گردانیم تا صفحه بندی داشته باشد.
اسکوپی با نام visibleTo
را صدا می زنیم که یک کاربر به عنوان ورودی می گیرد.
<?php
namespace App\Http\Controllers;
use App\Customer;
use App\User;
use Illuminate\Support\Facades\Auth;
class CustomersController extends Controller
{
public function index()
{
Auth::login(User::where('name', 'Sarah Seller')->first());
$customers = Customer::query()
->visibleTo(Auth::user())
->with('salesRep')
->orderBy('name')
->paginate();
return view('customers', ['customers' => $customers]);
}
}
اکنون باید این اسکوپ را پیاده سازی کنیم.
مانند همه اسکوپ ها در ورودی اول شی کوئری را می گیریم و ورودی دوم هم کاربر است.
در این متد یک where
به کوئری اضافه می کنیم تا تنها مشتری هایی خوانده شوند که مسئول فروش آن ها کاربر داده شده است. اما اگر کاربر ادمین باشد چطور؟ اگر کاربر ادمین باشد نیازی به ایجاد محدودیت نیست. قبل از اعمال محدودیت در کوئری چک می کنیم که اگر کاربر ادمین است محدودیت اعمال نشود.
public function scopeVisibleTo($query, User $user)
{
if (! $user->is_owner) {
$query->where('sales_rep_id', $user->id);
}
}
اکنون اگر صفحه را دوباره بارگذاری کنیم می توانیم ببینیم که صفحه بندی به درستی کار می کند. و اگر به کوئری ها نگاه کنیم می بینیم که تنها مشتریان یک مسئول فروش خاص، خوانده می شود.
حال اگر به عنوان ادمین وارد شویم همه مشتری ها دیده می شوند و کوئری ما محدود نشده است.
این یک مثال ساده بود اما اسکوپ های مربوط به سیاست (مانند متدی که نوشتیم) زمانی که داده های زیادی داریم و اجرای سیاست ها در سطح کد منطقی نیست به خوبی کار می کنند.
پروژه ی این بخش را می توانید از این اینجا دانلود کنید.