التعامل مع 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جدول المحتويات
- الفرق بين Authentication وAuthorization
- المشكلة الأمنية
- ما هي Gates وPolicies؟
- إنشاء Gate
- استخدام Gate داخل Controller
- استخدام Gate::authorize
- ما هي Policies؟
- إنشاء Policy
- دوال Policy
- التحقق من ملكية البيانات
- استخدام Policy داخل Controller
- حماية index وليس show فقط
- صلاحية إنشاء البيانات
- صلاحية تعديل البيانات
- صلاحية حذف البيانات
- استجابة 403 Forbidden
- إخفاء وجود Resource باستخدام 404
- السماح للـAdmin بجميع العمليات
- Authorization داخل Form Request
- إظهار الصلاحيات داخل API Resource
- منع IDOR / BOLA
- مثال عملي متكامل
- أفضل الممارسات
- ملخص الدرس
المشكلة الأمنية
لنفترض أن لدينا:
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-reportsPolicies
تستخدم لتنظيم صلاحيات 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);
}ما هي 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 لحماية بيانات المستخدمين.
التعليقات (0)
لا توجد تعليقات بعد — كن أول من يشارك رأيه.
أضف تعليقك