التعامل مع API في Laravel – الجزء السادس: حماية روابط API باستخدام API Key وMiddleware

في الأجزاء السابقة من سلسلة Laravel API قمنا بإنشاء REST API يستطيع جلب البيانات وإضافتها وتعديلها وحذفها، كما تعرفنا على Versioning وRate Limiting.

لكن هناك سؤال مهم:

هل نريد أن يستطيع أي شخص يعرف رابط API إرسال Requests إليه؟

في بعض الحالات قد نملك API مخصصًا للتواصل بين أنظمة موثوقة، مثل Backend يتواصل مع Backend آخر، ونريد إضافة طبقة بسيطة تمنع تنفيذ الطلب ما لم يرسل Client مفتاحًا سريًا صحيحًا.

إحدى الطرق المستخدمة في هذا السيناريو هي:

API Key Authentication

حيث يقوم Client بإرسال مفتاح سري مع كل Request، ويقوم Laravel بالتحقق منه قبل السماح للطلب بالوصول إلى Controller.

في هذا الدرس سنتعرف على كيفية إنشاء Middleware مخصص للتحقق من API Key، وكيفية إرسال المفتاح داخل HTTP Header، وتسجيل Middleware بالطريقة الحديثة في Laravel، بالإضافة إلى توضيح الفرق بين API Key وبين Laravel Sanctum.

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

  1. ما المشكلة التي نحاول حلها؟
  2. ما هو API Key؟
  3. هل API Key هو كلمة مرور للمستخدم؟
  4. متى نستخدم API Key؟
  5. متى لا نستخدم API Key؟
  6. لماذا لا نرسل المفتاح داخل URL؟
  7. إرسال API Key داخل Header
  8. إنشاء API Key قوي
  9. تخزين المفتاح في Environment
  10. إضافة المفتاح إلى Configuration
  11. إنشاء Middleware
  12. كتابة منطق التحقق
  13. لماذا نستخدم hash_equals؟
  14. تسجيل Middleware في Laravel الحديث
  15. حماية Route واحدة
  16. حماية مجموعة Routes
  17. استخدام API Key مع Versioning
  18. اختبار API Key باستخدام Postman
  19. استجابة Unauthorized
  20. استخدام Authorization Bearer بدل X-API-Key
  21. ماذا لو كان لدينا أكثر من Client؟
  22. API Key Rotation
  23. لماذا لا نخزن API Key في Frontend أو تطبيق الهاتف؟
  24. API Key أم Laravel Sanctum؟
  25. دمج API Key مع Rate Limiting
  26. ضرورة استخدام HTTPS
  27. أفضل الممارسات الأمنية
  28. مثال عملي متكامل
  29. ملخص الدرس
  30. الخلاصة

ما المشكلة التي نحاول حلها؟

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

GET /api/v1/brands

إذا لم يكن Route محميًا بأي نوع من Authentication، فإن أي Client يستطيع إرسال Request إليه والوصول إلى البيانات التي يسمح بها Endpoint.

لكن ماذا لو كان هذا API مخصصًا فقط لخادم آخر نملكه؟

مثل:

CRM Server
     |
     | API Request
     v
Laravel Application

هنا يمكن أن نطلب من CRM Server إرسال Secret Key مع كل Request.

إذا كان المفتاح صحيحًا:

Request
   |
   v
API Key Middleware
   |
   | Valid
   v
Controller

أما إذا كان المفتاح غير صحيح:

Request
   |
   v
API Key Middleware
   |
   | Invalid
   v
401 Unauthorized

ما هو API Key؟

API Key هو قيمة سرية يستخدمها Client للتعريف بنفسه أو لإثبات أنه مسموح له باستخدام API معين.

مثلًا:

X-API-Key: random-secret-key

Laravel يستقبل Request، ثم يقوم Middleware بمقارنة المفتاح المرسل بالمفتاح الذي نثق به.

إذا تطابق المفتاح يسمح للطلب بالمرور.

هل API Key هو كلمة مرور للمستخدم؟

لا.

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

User Authentication

و:

Application / Service Authentication

اسم المستخدم وكلمة المرور عادةً يمثلان مستخدمًا حقيقيًا.

أما API Key البسيط في هذا الدرس فيمثل Client أو Service مسموحًا له بالاتصال بالـAPI.

مثلًا:

User
   |
   | Email + Password
   v
Authentication


CRM Server
   |
   | API Key
   v
API

متى نستخدم API Key؟

يمكن أن يكون API Key مناسبًا في سيناريوهات مثل:

  • Server-to-Server Integration.
  • Webhook Integration عندما يكون التصميم مبنيًا على Secret مناسب.
  • API داخلي بين نظامين موثوقين.
  • خدمات داخلية لا تحتاج إلى حساب مستخدم مستقل.
  • التعريف بالـClient الذي يستهلك API.

على سبيل المثال:

Website Backend
      |
      | X-API-Key
      v
Internal Laravel API

متى لا يكون API Key الثابت مناسبًا؟

API Key الثابت ليس حلًا مثاليًا لجميع أنواع Authentication.

إذا كان لدينا مستخدمون يسجلون الدخول إلى تطبيق Android أو iOS أو SPA، فالأفضل استخدام نظام Authentication مصمم للمستخدمين مثل:

Laravel Sanctum

خصوصًا عندما نحتاج إلى:

  • معرفة المستخدم الحالي.
  • إصدار Tokens مختلفة لكل مستخدم.
  • إلغاء Token معين.
  • Token Abilities.
  • تسجيل الخروج.
  • إدارة Sessions.

لماذا لا نرسل API Key داخل URL؟

قد نرى أحيانًا طريقة مثل:

GET /api/v1/brands?api_password=my-secret-key

هذه الطريقة غير مفضلة أمنيًا.

Query String قد يتم تسجيلها في عدة أماكن، مثل:

  • Web Server Logs.
  • Reverse Proxy Logs.
  • Browser History.
  • Monitoring Systems.
  • Analytics Tools.
  • Debugging Logs.

لذلك لا ينبغي وضع Secrets الحساسة داخل URL كلما كان بالإمكان تجنب ذلك.

الأفضل إرسالها في Header.

إنشاء API Key قوي

لا تستخدم قيمة سهلة مثل:

123456

أو:

password

أو:

my_api_password

API Key يجب أن يحتوي على Entropy كافية وأن يكون من الصعب تخمينه.

يمكن مثلًا إنشاء قيمة عشوائية داخل Laravel باستخدام:

php artisan tinker

ثم:

use Illuminate\Support\Str;

Str::random(64);

سنفترض أن الناتج كان:

YOUR_RANDOM_64_CHARACTER_API_KEY

القيمة هنا مجرد مثال، ويجب توليد Secret حقيقي خاص بالتطبيق وعدم نشره في المقالات أو Git Repository.

تخزين API Key داخل .env

يمكن وضع المفتاح داخل:

.env

مثل:

INTERNAL_API_KEY=YOUR_RANDOM_64_CHARACTER_API_KEY

يجب عدم رفع ملف:

.env

إلى Git Repository.

كما يجب عدم كتابة المفتاح الحقيقي مباشرة داخل Middleware.

إضافة API Key إلى Configuration

بدل استدعاء:

env()

مباشرة داخل Middleware، من الأفضل وضع القيمة داخل Configuration ثم استخدام:

config()

مثلًا يمكن إنشاء ملف:

config/api.php

ووضع:

<?php

return [

    'key' => env('INTERNAL_API_KEY'),

];

بعد ذلك يمكن قراءة القيمة داخل التطبيق باستخدام:

config('api.key')

وهذا يجعل إعدادات التطبيق أكثر تنظيمًا ويتوافق بصورة أفضل مع Configuration Caching في بيئة Production.

إنشاء Middleware للتحقق من API Key

سننشئ Middleware باسم:

EnsureApiKeyIsValid

باستخدام:

php artisan make:middleware EnsureApiKeyIsValid

سيتم إنشاء الملف داخل:

app/Http/Middleware/EnsureApiKeyIsValid.php

بهيكل قريب من:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiKeyIsValid
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {
        return $next($request);
    }
}

كتابة منطق التحقق من API Key

نريد الحصول على المفتاح من:

X-API-Key

ثم مقارنته بالمفتاح الموجود داخل Configuration.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiKeyIsValid
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {

        $providedKey = $request->header('X-API-Key');

        $validKey = config('api.key');


        if (
            ! is_string($providedKey) ||
            ! is_string($validKey) ||
            ! hash_equals($validKey, $providedKey)
        ) {
            return response()->json([
                'message' => 'Unauthorized.',
            ], 401);
        }


        return $next($request);
    }
}

أصبح Middleware الآن يقوم بثلاث خطوات:

  1. قراءة API Key من Header.
  2. قراءة المفتاح الصحيح من Configuration.
  3. السماح للـRequest أو رفضه.

لماذا نستخدم hash_equals؟

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

$providedKey === $validKey

لكن عند مقارنة Secrets من الأفضل استخدام:

hash_equals()

وهي دالة PHP مصممة لمقارنة Strings بطريقة تقلل تسريب معلومات عن المقارنة عبر Timing Differences.

لذلك استخدمنا:

hash_equals(
    $validKey,
    $providedKey
)

تسجيل Middleware في Laravel الحديث

في الإصدارات الحديثة من Laravel يتم تسجيل Middleware aliases في:

bootstrap/app.php

وليس في:

app/Http/Kernel.php

نضيف:

use App\Http\Middleware\EnsureApiKeyIsValid;
use Illuminate\Foundation\Configuration\Middleware;

ثم داخل:

->withMiddleware()

نكتب:

->withMiddleware(
    function (Middleware $middleware): void {

        $middleware->alias([
            'api.key' => EnsureApiKeyIsValid::class,
        ]);

    }
)

أصبح لدينا Middleware Alias باسم:

api.key

حماية Route واحدة

يمكن الآن كتابة:

Route::get(
    '/brands',
    [BrandController::class, 'index']
)->middleware('api.key');

أي Request إلى Endpoint يجب أن يجتاز Middleware أولًا.

حماية مجموعة Routes

في الغالب نريد حماية مجموعة كاملة من Endpoints.

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

Route::middleware('api.key')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

وبذلك يتم تطبيق API Key Authentication على جميع Routes الخاصة بـBrands.

استخدام API Key مع API Versioning

في درس Versioning أنشأنا Routes مثل:

/api/v1/brands

/api/v2/brands

يمكن دمج Versioning مع API Key بسهولة:

Route::prefix('v1')
    ->middleware('api.key')
    ->group(function () {

        Route::apiResource(
            'brands',
            V1BrandController::class
        );

    });

الآن:

GET /api/v1/brands

لن يعمل إلا إذا تم إرسال API Key الصحيح.

اختبار API Key باستخدام Postman

إذا أرسلنا:

GET /api/v1/brands

بدون API Key، نحصل على:

401 Unauthorized

لاختبار الطلب من Postman نذهب إلى:

Headers

ثم نضيف:

Key:
X-API-Key

Value:
YOUR_RANDOM_64_CHARACTER_API_KEY

ثم نرسل Request.

إذا كان المفتاح صحيحًا يصل Request إلى Controller.

استجابة Unauthorized

إذا لم يتم إرسال المفتاح أو كان غير صحيح، يعيد Middleware:

{
    "message": "Unauthorized."
}

مع:

401 Unauthorized

ولا يتم تنفيذ Controller.

لاحظ أن Response لا تخبر Client:

  • ما هو المفتاح الصحيح.
  • أي جزء من المفتاح خاطئ.
  • هل المفتاح الموجود قريب من القيمة الصحيحة.

وهذا سلوك أفضل من إعطاء معلومات غير ضرورية للمستخدم غير المصرح له.

استخدام Authorization Bearer بدل X-API-Key

هناك تصميم آخر يمكن استخدامه وهو إرسال Secret داخل:

Authorization

بالشكل:

Authorization: Bearer YOUR_API_KEY

Laravel يستطيع قراءة Bearer Token من Request.

مثلًا:

$providedKey = $request->bearerToken();

فيصبح Middleware:

public function handle(
    Request $request,
    Closure $next
): Response {

    $providedKey = $request->bearerToken();

    $validKey = config('api.key');


    if (
        ! is_string($providedKey) ||
        ! is_string($validKey) ||
        ! hash_equals($validKey, $providedKey)
    ) {
        return response()->json([
            'message' => 'Unauthorized.',
        ], 401);
    }


    return $next($request);
}

لكن يجب الانتباه إلى أن استخدام كلمة:

Bearer

لا يحول المفتاح تلقائيًا إلى نظام Tokens متكامل.

في هذا المثال ما زلنا نقارن Secret ثابتًا، وليس Laravel Sanctum Personal Access Token.

ماذا لو كان لدينا أكثر من Client؟

استخدام API Key واحدة مشتركة بين جميع الأنظمة بسيط لكنه محدود.

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

CRM
Mobile Backend
Reporting Server
Partner Integration

إذا استخدمت جميعها المفتاح نفسه، ثم تسرب المفتاح، سنحتاج إلى تغييره لجميع الأنظمة.

كما أننا لن نعرف بسهولة أي Client قام بإرسال Request.

في الأنظمة الأكثر تقدمًا يفضل أن يكون لكل Client Credential مستقل.

مثلًا:

Client A
   |
   +--- API Key A


Client B
   |
   +--- API Key B


Client C
   |
   +--- API Key C

عندها يمكن:

  • إلغاء Client واحد فقط.
  • تطبيق Rate Limit مختلف لكل Client.
  • تسجيل استخدام كل Client.
  • إعطاء صلاحيات مختلفة.
  • تدوير المفتاح بشكل مستقل.

ما هو API Key Rotation؟

API Key لا ينبغي اعتباره Secret سيبقى إلى الأبد.

من الأفضل وجود آلية لتغييره عند الحاجة، خصوصًا إذا:

  • تم الاشتباه بتسريبه.
  • غادر أحد الأشخاص الذين لديهم صلاحية الوصول.
  • ظهر المفتاح في Log أو Repository بالخطأ.
  • تحتاج سياسة المؤسسة إلى تغيير Secrets دوريًا.

وتسمى عملية استبدال Secret:

Key Rotation

في الأنظمة الكبيرة قد ندعم مفتاحين مؤقتًا:

Current Key

New Key

ثم يتم تحديث Clients وإلغاء المفتاح القديم.

لماذا لا نخزن Secret API Key داخل Frontend أو تطبيق الهاتف؟

هذه نقطة أمنية مهمة جدًا.

إذا وضعت API Key ثابتًا داخل:

  • JavaScript Frontend.
  • React Application.
  • Vue Application.
  • Flutter Application.
  • Android APK.
  • iOS Application.

فيجب ألا تفترض أن المفتاح سيبقى سريًا.

الكود الذي يصل إلى جهاز المستخدم يمكن تحليله أو مراقبة Requests الخاصة به، وبالتالي يمكن استخراج Credentials الثابتة في كثير من الحالات.

لذلك API Key ثابت مناسب أكثر لسيناريو:

Server
   |
   | Secret
   v
Server

وليس كبديل عن User Authentication داخل تطبيق يتم توزيعه للمستخدمين.

API Key أم Laravel Sanctum؟

يعتمد الاختيار على ما تريد حمايته.

الحالةالحل المناسب غالبًا
Server يتصل بـServer داخليAPI Key أو آلية Service Authentication مناسبة
مستخدم يسجل الدخول من تطبيق الهاتفLaravel Sanctum
SPA تابع لنفس التطبيقSanctum SPA Authentication
Personal Access TokensLaravel Sanctum
صلاحيات Token لكل مستخدمSanctum Token Abilities
OAuth2 كاملحل OAuth مناسب مثل Laravel Passport عند الحاجة

API Key البسيط لا يوفر تلقائيًا:

  • User Authentication.
  • Token Revocation لكل مستخدم.
  • Token Abilities.
  • Sessions.
  • OAuth Flows.

لذلك يجب اختيار التقنية بناءً على نوع Client وليس فقط على سهولة التطبيق.

دمج API Key مع Rate Limiting

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

هل هذا Client مسموح له بالوصول؟

أما Rate Limiting فيجيب عن:

كم عدد Requests التي يستطيع إرسالها؟

لذلك يمكن استخدام الطبقتين معًا:

Route::middleware([
    'api.key',
    'throttle:api',
])->group(function () {

    Route::apiResource(
        'brands',
        BrandController::class
    );

});

ويصبح التدفق:

Request
   |
   v
API Key Check
   |
   v
Rate Limiting
   |
   v
Controller

ضرورة استخدام HTTPS

نقل API Key داخل Header لا يعني أن الاتصال أصبح آمنًا إذا تم إرسال الطلب عبر HTTP غير مشفر.

يجب استخدام:

HTTPS

حتى يتم تشفير الاتصال أثناء انتقال البيانات بين Client وServer.

ولا ينبغي إرسال Secrets عبر:

http://

في بيئة Production.

أفضل الممارسات الأمنية

لا ترسل API Key داخل Query String

تجنب:

?api_key=secret

واستخدم Header.

لا تضع مفتاحًا افتراضيًا داخل Source Code

تجنب:

env(
    'API_KEY',
    'default-password'
)

لأن وجود Default Secret معروف يعني أن خطأ Configuration قد يؤدي إلى استخدام مفتاح يمكن توقعه.

إذا لم يكن Secret مضبوطًا، ارفض الطلب

Configuration المفقودة يجب ألا تؤدي إلى تشغيل النظام باستخدام Password افتراضية.

لا ترفع Secrets إلى Git

لا تضع:

API_KEY=real-secret

في Repository.

استخدم HTTPS دائمًا

خصوصًا عند إرسال Authentication Credentials.

استخدم Keys مختلفة للعملاء المختلفين عند الحاجة

هذا يسهل Revocation وAuditing وRate Limiting.

غير المفتاح إذا تم تسريبه

لا تعتمد على Secret معروف أنه تعرض للكشف.

أضف Rate Limiting

وجود Secret صحيح لا يعني السماح بعدد غير محدود من Requests.

سجل الاستخدام بدون تسجيل Secret نفسه

يمكن تسجيل:

  • Client ID.
  • Endpoint.
  • Timestamp.
  • Status Code.

لكن تجنب وضع API Key نفسه داخل Application Logs.

لا تستخدم Static API Key كبديل عن User Authentication

إذا كانت العملية مرتبطة بمستخدم حقيقي، فاستخدم Authentication مناسبًا مثل Sanctum حسب السيناريو.

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

1. إنشاء Secret

داخل:

.env

نضيف:

INTERNAL_API_KEY=YOUR_RANDOM_64_CHARACTER_API_KEY

2. إنشاء Configuration

الملف:

config/api.php

يحتوي على:

<?php

return [

    'key' => env('INTERNAL_API_KEY'),

];

3. إنشاء Middleware

php artisan make:middleware EnsureApiKeyIsValid

4. كتابة Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureApiKeyIsValid
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {

        $providedKey = $request->header(
            'X-API-Key'
        );

        $validKey = config('api.key');


        if (
            ! is_string($providedKey) ||
            ! is_string($validKey) ||
            ! hash_equals(
                $validKey,
                $providedKey
            )
        ) {
            return response()->json([
                'message' => 'Unauthorized.',
            ], 401);
        }


        return $next($request);
    }
}

5. تسجيل Middleware Alias

داخل:

bootstrap/app.php

نضيف:

use App\Http\Middleware\EnsureApiKeyIsValid;
use Illuminate\Foundation\Configuration\Middleware;

ثم:

->withMiddleware(
    function (Middleware $middleware): void {

        $middleware->alias([
            'api.key' => EnsureApiKeyIsValid::class,
        ]);

    }
)

6. حماية API Routes

داخل:

routes/api.php

نكتب:

Route::prefix('v1')
    ->middleware([
        'api.key',
        'throttle:api',
    ])
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

7. إرسال Request

GET /api/v1/brands

Accept: application/json
X-API-Key: YOUR_RANDOM_64_CHARACTER_API_KEY

8. API Key غير صحيح

Response:

HTTP 401 Unauthorized
{
    "message": "Unauthorized."
}

9. API Key صحيح

يمر Request عبر Middleware ويتم تنفيذ:

BrandController

ثم يعيد API البيانات المطلوبة.

ملخص الدرس

المفهومالوصف
API KeySecret يستخدم للسماح لـClient أو Service بالوصول إلى API.
X-API-KeyHTTP Header يمكن استخدامه لإرسال API Key.
Middlewareطبقة تفحص Request قبل وصوله إلى Controller.
hash_equals()طريقة مناسبة لمقارنة Strings حساسة مثل Secrets.
401 Unauthorizedاستجابة عند عدم تقديم Credentials صحيحة.
config()قراءة إعدادات التطبيق من Configuration.
.envمكان Environment Variables الخاصة بالبيئة.
bootstrap/app.phpالمكان الحديث لتسجيل Middleware aliases في Laravel.
Sanctumحل Authentication مناسب للمستخدمين وAPI Tokens.
Rate Limitingتحديد عدد Requests المسموح بها.
HTTPSتشفير الاتصال بين Client وServer.

الخلاصة

تعرفنا في هذا الجزء على طريقة إضافة طبقة حماية بسيطة إلى REST API باستخدام API Key وMiddleware مخصص.

بدل إرسال كلمة المرور داخل URL:

?api_password=secret

استخدمنا Header:

X-API-Key: secret

ثم أنشأنا Middleware يقوم بقراءة المفتاح ومقارنته بالمفتاح الموجود داخل Configuration قبل السماح للـRequest بالوصول إلى Controller.

كما سجلنا Middleware Alias بالطريقة الحديثة داخل:

bootstrap/app.php

واستخدمناه مع Routes:

middleware('api.key')

وتعرفنا أيضًا على نقطة مهمة: API Key الثابت ليس بديلًا عن نظام Authentication للمستخدمين.

إذا كان لدينا تطبيق هاتف أو SPA يحتوي على حسابات مستخدمين، فإن حلولًا مثل Laravel Sanctum تكون أكثر ملاءمة لأنها توفر Tokens مرتبطة بالمستخدمين ويمكن إدارتها وإلغاؤها وتحديد صلاحياتها.

أما API Key البسيط فيكون مفيدًا أكثر عندما نحتاج إلى حماية الاتصال بين خدمات أو Servers موثوقة.

وأخيرًا، API Key يجب ألا يكون طبقة الحماية الوحيدة. يمكن دمجه مع HTTPS وRate Limiting وAuthentication وAuthorization وLogging حسب حساسية النظام وطبيعة API.