التعامل مع API في Laravel – الجزء التاسع: Authorization باستخدام Policies وGates وحماية البيانات حسب المستخدم

بعد أن قمنا بحماية REST API والتأكد من هوية المستخدم من خلال Authentication، تظهر لدينا مشكلة أمنية أخرى لا تقل أهمية:

هل المستخدم الذي قام بتسجيل الدخول يملك فعلًا صلاحية تنفيذ العملية المطلوبة؟

وجود Access Token صحيح لا يعني أن المستخدم يجب أن يستطيع مشاهدة أو تعديل أو حذف جميع البيانات الموجودة في النظام.

لنفترض أن لدينا متجرًا يحتوي على الطلبات التالية:

Order #100 → User #5
Order #101 → User #8
Order #102 → User #12

إذا كان User #5 مسجلًا في التطبيق، فمن الطبيعي أن يستطيع مشاهدة:

GET /api/orders/100

لكن ماذا يحدث إذا قام بتغيير الرقم يدويًا وأرسل:

GET /api/orders/101

إذا كان Backend يتحقق فقط من Authentication، فقد يحصل المستخدم على بيانات مستخدم آخر.

هنا يأتي دور:

Authorization

وفي Laravel توجد طريقتان أساسيتان لتنظيم Authorization:

Gates
Policies

جدول المحتويات

  1. الفرق بين Authentication وAuthorization
  2. المشكلة الأمنية
  3. ما هي Gates وPolicies؟
  4. إنشاء Gate
  5. استخدام Gate داخل Controller
  6. استخدام Gate::authorize
  7. ما هي Policies؟
  8. إنشاء Policy
  9. دوال Policy
  10. التحقق من ملكية البيانات
  11. استخدام Policy داخل Controller
  12. حماية index وليس show فقط
  13. صلاحية إنشاء البيانات
  14. صلاحية تعديل البيانات
  15. صلاحية حذف البيانات
  16. استجابة 403 Forbidden
  17. إخفاء وجود Resource باستخدام 404
  18. السماح للـAdmin بجميع العمليات
  19. Authorization داخل Form Request
  20. إظهار الصلاحيات داخل API Resource
  21. منع IDOR / BOLA
  22. مثال عملي متكامل
  23. أفضل الممارسات
  24. ملخص الدرس

الفرق بين Authentication وAuthorization

هناك فرق مهم جدًا بين المصطلحين.

Authentication

يجيب عن السؤال:

من أنت؟

مثل التحقق من Access Token ومعرفة المستخدم الحالي.

Authorization

يجيب عن السؤال:

ماذا يسمح لك أن تفعل؟

قد يكون المستخدم مسجلًا بشكل صحيح، لكنه لا يملك صلاحية تعديل Order معين.

مثلًا:

Authentication
      |
      v
User #5
      |
      v
Authorization
      |
      +--- Order #100 → Allowed
      |
      +--- Order #101 → Denied

المشكلة الأمنية

لنفترض أن لدينا:

Route::get(
    '/orders/{order}',
    [OrderController::class, 'show']
);

وفي Controller:

public function show(Order $order)
{
    return new OrderResource($order);
}

Route Model Binding سيقوم بجلب Order المطلوب، لكنه لا يعني تلقائيًا أن المستخدم الحالي يملك هذا Order.

لذلك يجب إضافة Authorization.

ما الفرق بين Gates وPolicies؟

Laravel يوفر الطريقتين.

Gates

مناسبة غالبًا للصلاحيات البسيطة أو العمليات التي لا ترتبط مباشرة بـModel محدد.

مثل:

view-admin-dashboard
manage-settings
view-reports

Policies

تستخدم لتنظيم صلاحيات Model أو Resource معين.

مثل:

OrderPolicy
ProductPolicy
PostPolicy
InvoicePolicy

إذا كانت الصلاحية مرتبطة بعمليات:

view
create
update
delete

على Model، فإن Policy تكون عادةً الاختيار الأنسب.

إنشاء Gate

لنفترض أننا نريد السماح للـAdmin فقط بمشاهدة Dashboard معينة.

يمكن تعريف Gate داخل:

app/Providers/AppServiceProvider.php

مثل:

use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define(
        'view-admin-dashboard',
        function (User $user) {
            return $user->is_admin;
        }
    );
}

Gate ستعيد:

true

إذا كان المستخدم Admin، و:

false

إذا لم يكن كذلك.

استخدام Gate داخل Controller

يمكن التحقق باستخدام:

Gate::allows()

مثل:

if (! Gate::allows('view-admin-dashboard')) {
    abort(403);
}

أو:

if (Gate::denies('view-admin-dashboard')) {
    abort(403);
}

استخدام Gate::authorize()

بدل كتابة شرط يدوي في كل مرة، يمكن استخدام:

Gate::authorize(
    'view-admin-dashboard'
);

إذا كان المستخدم غير مصرح له، يقوم Laravel بإطلاق AuthorizationException وتحويلها إلى HTTP Response مناسب.

أما إذا كان مسموحًا له، فيستمر تنفيذ الكود.

ما هي Policies؟

Policy عبارة عن Class يحتوي على قواعد Authorization الخاصة بـModel.

إذا كان لدينا:

Order

يمكن إنشاء:

OrderPolicy

وتحتوي على صلاحيات مثل:

viewAny()
view()
create()
update()
delete()
restore()
forceDelete()

إنشاء OrderPolicy

يمكن استخدام:

php artisan make:policy OrderPolicy --model=Order

سيتم إنشاء:

app/Policies/OrderPolicy.php

عند اتباع Laravel Naming Conventions مثل:

App\Models\Order
App\Policies\OrderPolicy

يستطيع Laravel اكتشاف Policy تلقائيًا في البنية القياسية، لذلك لا تحتاج في الحالة المعتادة إلى تسجيلها يدويًا.

دوال OrderPolicy

يمكن أن تكون Policy بالشكل التالي:

<?php

namespace App\Policies;

use App\Models\Order;
use App\Models\User;

class OrderPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }


    public function view(
        User $user,
        Order $order
    ): bool {

        return $user->id === $order->user_id;
    }


    public function create(User $user): bool
    {
        return true;
    }


    public function update(
        User $user,
        Order $order
    ): bool {

        return $user->id === $order->user_id;
    }


    public function delete(
        User $user,
        Order $order
    ): bool {

        return $user->id === $order->user_id;
    }
}

التحقق من ملكية البيانات

أهم جزء في المثال هو:

return $user->id === $order->user_id;

هذا يعني:

إذا كان:

Authenticated User ID = 5
Order User ID = 5

فالنتيجة:

true

أما:

Authenticated User ID = 5
Order User ID = 8

فالنتيجة:

false

استخدام Policy داخل Controller

يمكن استخدام Gate مع Policy:

use Illuminate\Support\Facades\Gate;

public function show(Order $order)
{
    Gate::authorize(
        'view',
        $order
    );

    return new OrderResource(
        $order
    );
}

Laravel سيبحث عن Policy المرتبطة بـOrder ويقوم باستدعاء:

OrderPolicy::view()

حماية index وليس show فقط

من أكثر الأخطاء شيوعًا حماية:

GET /orders/{order}

ثم ترك:

GET /orders

يعيد جميع Orders الموجودة في قاعدة البيانات.

الخطأ:

Order::all();

في API خاص بالمستخدم.

الأصح مثلًا:

public function index(Request $request)
{
    $orders = Order::query()
        ->where(
            'user_id',
            $request->user()->id
        )
        ->latest()
        ->get();

    return OrderResource::collection(
        $orders
    );
}

Authorization لا تعني فقط رفض الوصول إلى Record بعد جلبه، بل يجب أيضًا تصميم Queries بحيث لا تكشف بيانات المستخدمين الآخرين.

صلاحية إنشاء البيانات

عند إنشاء Order لا يوجد Order Model بعد.

لذلك نتحقق من Class:

Gate::authorize(
    'create',
    Order::class
);

ثم ننشئ Order:

$order = $request
    ->user()
    ->orders()
    ->create(
        $request->validated()
    );

استخدام علاقة المستخدم لإنشاء Order يساعد أيضًا في منع Client من اختيار:

user_id

بنفسه.

صلاحية تعديل البيانات

public function update(
    UpdateOrderRequest $request,
    Order $order
) {
    Gate::authorize(
        'update',
        $order
    );

    $order->update(
        $request->validated()
    );

    return new OrderResource(
        $order->refresh()
    );
}

قبل تنفيذ:

$order->update()

يتم التحقق من Policy.

صلاحية حذف البيانات

public function destroy(Order $order)
{
    Gate::authorize(
        'delete',
        $order
    );

    $order->delete();

    return response()->noContent();
}

إذا كان Order يخص مستخدمًا آخر، فلن يصل التنفيذ إلى:

delete()

استجابة 403 Forbidden

إذا كان المستخدم Authenticated لكنه لا يملك الصلاحية، تكون الاستجابة المعتادة:

403 Forbidden

وهنا يجب التفريق بين:

Statusالمعنى
401المستخدم غير موثق Authentication.
403المستخدم معروف، لكنه غير مخول لتنفيذ العملية.

إخفاء وجود Resource باستخدام 404

في بعض APIs الحساسة قد لا نريد إخبار المستخدم أصلًا أن Resource موجود.

Laravel يسمح بإرجاع Authorization Response كـNot Found.

مثل:

use Illuminate\Auth\Access\Response;

public function view(
    User $user,
    Order $order
): Response {

    return $user->id === $order->user_id
        ? Response::allow()
        : Response::denyAsNotFound();
}

بدل:

403 Forbidden

سيظهر:

404 Not Found

وهذا مفيد عندما لا نريد كشف وجود Record لمستخدم غير مصرح له.

السماح للـAdmin بجميع العمليات

قد نريد أن يستطيع Administrator تجاوز Policies العادية.

يمكن استخدام:

Gate::before()

داخل AppServiceProvider:

Gate::before(
    function (User $user, string $ability) {

        if ($user->is_admin) {
            return true;
        }

        return null;
    }
);

إذا كان المستخدم Admin يتم السماح له قبل تنفيذ Policy.

أما إعادة:

null

فتعني الاستمرار في Authorization الطبيعي.

Authorization داخل Form Request

Form Request لا يحتوي فقط على:

rules()

بل يمكن استخدام:

authorize()

مثل:

public function authorize(): bool
{
    return $this->user()->can(
        'update',
        $this->route('order')
    );
}

وبذلك يجمع Form Request بين:

Authorization
+
Validation

مع بقاء كل مسؤولية منفصلة منطقيًا.

إظهار الصلاحيات داخل API Resource

أحيانًا يحتاج Frontend إلى معرفة العمليات التي يستطيع المستخدم تنفيذها.

يمكن مثلًا إرجاع:

'permissions' => [
    'update' =>
        $request->user()
            ?->can('update', $this->resource)
            ?? false,

    'delete' =>
        $request->user()
            ?->can('delete', $this->resource)
            ?? false,
],

فتصبح Response:

{
    "id": 100,
    "status": "pending",

    "permissions": {
        "update": true,
        "delete": false
    }
}

يمكن للواجهة استخدام هذه المعلومات لإخفاء أو إظهار Buttons.

لكن هذه نقطة مهمة:

إخفاء زر Delete في Frontend ليس Authorization.

يجب دائمًا إعادة التحقق من الصلاحية في Backend.

منع IDOR / BOLA في Laravel API

من أخطر الأخطاء في APIs الاعتماد على Authentication فقط.

مثلًا:

GET /api/orders/100

ثم يستطيع المستخدم تغيير الرقم:

GET /api/orders/101
GET /api/orders/102
GET /api/orders/103

والحصول على بيانات الآخرين.

هذا النوع من المشاكل يعرف عادةً ضمن مشاكل Broken Object Level Authorization.

الحل ليس إخفاء IDs أو تحويلها إلى UUID فقط.

حتى لو كان Identifier:

550e8400-e29b-41d4-a716-446655440000

يجب أن يتحقق Backend من صلاحية المستخدم للوصول إلى Resource.

Policies تساعد على وضع هذه القاعدة في مكان مركزي ومنظم.

مثال عملي متكامل

OrderPolicy

<?php

namespace App\Policies;

use App\Models\Order;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class OrderPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }


    public function view(
        User $user,
        Order $order
    ): Response {

        return $user->id === $order->user_id
            ? Response::allow()
            : Response::denyAsNotFound();
    }


    public function create(User $user): bool
    {
        return true;
    }


    public function update(
        User $user,
        Order $order
    ): bool {

        return $user->id === $order->user_id;
    }


    public function delete(
        User $user,
        Order $order
    ): bool {

        return $user->id === $order->user_id;
    }
}

Routes

Route::middleware('auth:sanctum')
    ->group(function () {

        Route::apiResource(
            'orders',
            OrderController::class
        );

    });

Controller

class OrderController extends Controller
{
    public function index(Request $request)
    {
        $orders = Order::query()
            ->where(
                'user_id',
                $request->user()->id
            )
            ->latest()
            ->get();

        return OrderResource::collection(
            $orders
        );
    }


    public function show(Order $order)
    {
        Gate::authorize(
            'view',
            $order
        );

        return new OrderResource(
            $order
        );
    }


    public function update(
        UpdateOrderRequest $request,
        Order $order
    ) {
        Gate::authorize(
            'update',
            $order
        );

        $order->update(
            $request->validated()
        );

        return new OrderResource(
            $order->refresh()
        );
    }


    public function destroy(Order $order)
    {
        Gate::authorize(
            'delete',
            $order
        );

        $order->delete();

        return response()->noContent();
    }
}

بهذا أصبح لدينا أكثر من طبقة:

Request
   |
   v
Authentication
   |
   v
Authenticated User
   |
   v
Authorization / Policy
   |
   +---- Denied → 403 / 404
   |
   v
Controller
   |
   v
Database
   |
   v
API Resource
   |
   v
JSON Response

أفضل الممارسات

  • لا تعتبر Authentication بديلًا عن Authorization.
  • استخدم Policies للصلاحيات المرتبطة بـModels.
  • استخدم Gates للصلاحيات العامة والبسيطة عندما يكون ذلك مناسبًا.
  • لا تثق بالـID القادم من Client.
  • لا تعتمد على إخفاء Buttons في Frontend.
  • قيد Queries حسب المستخدم عند عرض Collections.
  • لا تسمح للـClient بتحديد user_id للموارد التي يجب أن يمتلكها المستخدم الحالي.
  • استخدم 403 عند منع عملية، أو 404 عندما يكون إخفاء وجود Resource جزءًا من تصميمك الأمني.
  • ضع Authorization Logic في مكان مركزي بدل تكراره في Controllers.

ملخص الدرس

المفهومالوظيفة
Authenticationتحديد هوية المستخدم.
Authorizationتحديد العمليات التي يسمح للمستخدم بتنفيذها.
Gateقاعدة Authorization بسيطة.
Policyتنظيم صلاحيات Model أو Resource.
Gate::authorize()تنفيذ Authorization وإطلاق Exception عند الرفض.
403المستخدم لا يملك صلاحية العملية.
denyAsNotFound()إخفاء وجود Resource باستخدام 404.
Gate::before()تنفيذ Authorization Check قبل بقية القواعد.
BOLA / IDORالوصول إلى Object لا يملك المستخدم صلاحية الوصول إليه.

الخلاصة

حماية API لا تنتهي عند تسجيل دخول المستخدم والحصول على Access Token.

Authentication تخبرنا من هو المستخدم، بينما Authorization تحدد البيانات والعمليات التي يحق لهذا المستخدم الوصول إليها.

Laravel يوفر Gates وPolicies لتنظيم هذه الصلاحيات، وتعتبر Policies مناسبة بشكل خاص لحماية Models مثل Orders وPosts وInvoices.

عند تصميم API حقيقي، يجب تطبيق Authorization على كل عملية حساسة، وعدم الاعتماد على Frontend أو صعوبة تخمين IDs لحماية بيانات المستخدمين.