404 Not Found

404 Not Found


nginx

نظام المصادقة في Laravel—Sanctum

Sanctum هو "حارس البوابة" في Laravel—يتولى المصادقة القائمة على الجلسات لصفحات الويب والمصادقة القائمة على الرموز لواجهات API، مما يوفر نظامي مصادقة في إطار واحد.

1. ما ستتعلمه


2. قصة حقيقية لمهندس أمن

(1) نقطة الألم: حوادث أمنية ناتجة عن ارتباك مخططات مصادقة API

استخدم Bob مصادقة JWT لواجهة ShopMetrics API والجلسات للويب—نظاما مصادقة منفصلان، مع عدم مزامنة حالات تسجيل دخول المستخدمين. بعد أن سجّلت Alice دخولها عبر المتصفح، كان لا يزال عليها الحصول على رمز منفصل لاستدعاءات API، الذي كان ينتهي بشكل متكرر، مما يتسبب في انقطاع العمليات. والأخطر من ذلك، اكتشف Charlie أن رمز API واحداً يمكنه الوصول إلى بيانات جميع المستأجرين—لم يكن هناك عزل بين المستأجرين، مما يشكل خطراً كبيراً لتسريب البيانات عبر المستأجرين.

(2) حل "Sanctum"

يوحّد Sanctum مصادقة الويب وAPI—في وضع SPA، يتشارك المتصفح وAPI الجلسة نفسها؛ وفي وضع الرموز، يوفر رموز API لعملاء الطرف الثالث؛ وحقل abilities يسمح بالتحكم الدقيق في الصلاحيات.

PHP
// مصادقة SPA — مشاركة الجلسة بين الويب وAPI
// فقط تأكد من أن وسيط Sanctum موجود على مسارات API
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

// مصادقة بالرموز — لعملاء الطرف الثالث
$token = $user->createToken('api-token', ['read-orders', 'write-products']);
// الرمز يمكنه فقط ما تسمح به قدراته

(3) العائد

بعد تسجيل الدخول الموحد لـ Bob، تجربة SPA لـ Alice سلسة وشفافة، وصلاحيات رمز Charlie مقيّدة بدقة بـ "قراءة الطلبات فقط"، وخطر تسريب البيانات عبر المستأجرين انخفض إلى الصفر.


3. تثبيت وإعداد Sanctum

(1) التثبيت

BASH
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
# ينشئ: جدول personal_access_tokens

(2) الإعداد

PHP
// config/sanctum.php
'middleware' => [
    'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
    'encrypt_cookies'   => App\Http\Middleware\EncryptCookies::class,
],

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000',
    env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : '',
))),

'expiration' => null, // الرمز لا ينتهي أبداً (أو عيّن الدقائق)

(3) إعداد الواجهة الأمامية لـ SPA

JAVASCRIPT
// resources/js/bootstrap.js
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.withCredentials = true; // إرسال ملفات تعريف الارتباط مع طلبات API
خيار الإعداد الوظيفة القيمة الافتراضية
stateful نطاق مصادقة SPA localhost:3000، إلخ
expiration وقت انتهاء الرمز null (لا ينتهي أبداً)
guard الحارس المُتحقق web

(1) ▶ مثال: إعداد مصادقة Sanctum SPA

BASH
# .env — إعداد نطاقات SPA
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost:3000,localhost:5173
SESSION_DRIVER=redis

# تثبيت Sanctum والإعداد
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

4. عملية التسجيل/تسجيل الدخول/تسجيل الخروج

(1) التسجيل

PHP
// app/Http/Controllers/Auth/RegisterController.php
class RegisterController extends Controller
{
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'tenant_id' => $request->tenant_id,
            'role' => 'tenant_owner',
        ]);

        event(new Registered($user));

        Auth::login($user);

        return response()->json([
            'user' => $user,
            'message' => 'Registration successful.',
        ], 201);
    }
}

(2) تسجيل الدخول

PHP
// app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        if (!Auth::attempt($request->only('email', 'password'))) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $request->session()->regenerate();

        return response()->json([
            'user' => Auth::user(),
            'message' => 'Login successful.',
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        Auth::guard('web')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return response()->json(['message' => 'Logged out.']);
    }
}

(3) المصادقة وتسجيل الدخول بالرموز

PHP
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
    public function login(LoginRequest $request): JsonResponse
    {
        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Invalid credentials.'],
            ]);
        }

        $token = $user->createToken(
            $request->device_name ?? 'api-token',
            $request->abilities ?? ['*'],
        );

        return response()->json([
            'user' => $user,
            'token' => $token->plainTextToken,
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Token revoked.']);
    }
}
الوضع طريقة تسجيل الدخول استمرار الحالة السيناريوهات المناسبة
جلسة SPA نموذج تسجيل الدخول ملفات تعريف الارتباط/الجلسات واجهة SPA الأمامية
الرموز الحصول على plainTextToken سلسلة الرمز تطبيقات الموبايل/الطرف الثالث

(1) ▶ مثال: مسارات المصادقة ذات الوضع المزدوج لـ ShopMetrics

PHP
// routes/web.php — مصادقة الجلسة لـ SPA
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LogoutController::class, 'logout'])->middleware('auth');
Route::post('/register', [RegisterController::class, 'register']);

// routes/api.php — مصادقة الرموز لـ API
Route::post('/tokens', [ApiTokenController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
    Route::delete('/tokens/current', [ApiTokenController::class, 'logout']);
    Route::get('/user', fn (Request $request) => $request->user());
});

الناتج:

TEXT
// تم التنفيذ بنجاح

5. قدرات الرموز والصلاحيات

(1) إنشاء رمز بقدرات (abilities)

PHP
// إنشاء رمز بقدرات محددة
$token = $user->createToken('analytics-read', [
    'read-analytics',
    'read-orders',
]);

// رمز وصول كامل
$token = $user->createToken('admin-token', ['*']);

// رمز مع انتهاء الصلاحية
$token = $user->createToken('temp-token', ['read-orders'], now()->addDays(7));

(2) التحقق من قدرات الرمز

PHP
// في المتحكم أو الوسيط
if ($request->user()->tokenCan('read-analytics')) {
    return AnalyticsResource::collection($analytics);
}

// في الوسيط
class CheckAbilities
{
    public function handle($request, $next, ...$abilities)
    {
        foreach ($abilities as $ability) {
            if (!$request->user()->tokenCan($ability)) {
                abort(403, 'Insufficient permissions.');
            }
        }
        return $next($request);
    }
}

(3) مصفوفة صلاحيات القدرات (Abilities)

الدور القدرات الوصف
المسؤول الأعلى * صلاحيات كاملة
مالك المستأجر read-*, write-* صلاحيات كاملة ضمن المستأجر
محلل read-analytics, read-orders قراءة فقط
عميل API read-products, write-orders طرف ثالث فقط

(1) ▶ مثال: التحكم في وصول رموز ShopMetrics

PHP
// app/Http/Controllers/Api/TokenController.php
class TokenController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'abilities' => 'sometimes|array',
            'abilities.*' => 'in:read-analytics,read-orders,write-orders,read-products,write-products',
            'expires_in_days' => 'sometimes|integer|min:1|max:365',
        ]);

        $abilities = $validated['abilities'] ?? match ($request->user()->role) {
            'super_admin' => ['*'],
            'tenant_owner' => ['read-analytics', 'read-orders', 'write-orders', 'read-products', 'write-products'],
            'analyst' => ['read-analytics', 'read-orders', 'read-products'],
            default => [],
        };

        $expiresAt = isset($validated['expires_in_days'])
            ? now()->addDays($validated['expires_in_days'])
            : null;

        $token = $request->user()->createToken(
            $validated['name'],
            $abilities,
            $expiresAt,
        );

        return response()->json([
            'token' => $token->plainTextToken,
            'abilities' => $abilities,
        ], 201);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. عزل المصادقة متعدد المستأجرين

(1) مخطط تسلسل مصادقة رموز Sanctum

100%
sequenceDiagram
    participant C as العميل
    participant S as Sanctum
    participant DB as قاعدة البيانات
    participant M as الوسيط

    C->>S: POST /api/tokens (البريد + كلمة المرور)
    S->>DB: البحث عن المستخدم + التحقق من كلمة المرور
    DB-->>S: تم العثور على المستخدم
    S->>DB: إنشاء personal_access_token
    DB-->>S: تم إنشاء الرمز
    S-->>C: إرجاع plainTextToken

    C->>M: GET /api/shops (رمز Bearer)
    M->>DB: البحث عن الرمز في personal_access_tokens
    DB-->>M: الرمز + المستخدم + القدرات
    M->>M: التحقق من قدرة tokenCan()
    M->>M: التحقق من نطاق المستأجر
    M-->>C: إرجاع بيانات نطاق المستأجر

(2) وسيط مصادقة المستأجر

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();
        if (!$user || !$user->tenant_id) {
            abort(403, 'No tenant associated.');
        }

        $tenant = $user->tenant;
        if ($tenant->status !== 'active') {
            abort(403, 'Tenant account is suspended.');
        }

        // تعيين سياق المستأجر لجميع الاستعلامات
        app()->instance(Tenant::class, $tenant);

        return $next($request);
    }
}

(3) نطاق المستأجر المتعدد

PHP
// app/Models/TenantScope.php
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenant = app()->make(Tenant::class)) {
            $builder->where($model->getTable() . '.tenant_id', $tenant->id);
        }
    }
}

// تطبيق على النماذج
class Shop extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new TenantScope);
    }
}

(1) ▶ مثال: مسارات مصادقة ShopMetrics متعددة المستأجرين

PHP
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    // جميع مسارات API تتطلب تحليل المستأجر
    Route::middleware('tenant.resolve')->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
        Route::apiResource('products', Api\ProductController::class);
        Route::apiResource('orders', Api\OrderController::class)
            ->only(['index', 'show', 'update']);

        // التحليلات — تتطلب قدرة محددة
        Route::get('analytics', [Api\AnalyticsController::class, 'index'])
            ->middleware('ability:read-analytics');

        // مسارات المسؤول فقط
        Route::middleware('ability:*')->prefix('admin')->group(function () {
            Route::apiResource('plans', Api\Admin\PlanController::class);
            Route::apiResource('tenants', Api\Admin\TenantController::class);
        });
    });
});

الناتج:

TEXT
// تم التنفيذ بنجاح

7. إعادة تعيين كلمة المرور والتحقق من البريد الإلكتروني

(1) إعادة تعيين كلمة المرور

PHP
// routes/web.php
Route::post('/forgot-password', [PasswordResetController::class, 'sendResetLink']);
Route::post('/reset-password', [PasswordResetController::class, 'reset']);

// app/Http/Controllers/PasswordResetController.php
class PasswordResetController extends Controller
{
    public function sendResetLink(Request $request): JsonResponse
    {
        $request->validate(['email' => 'required|email|exists:users']);
        $status = Password::sendResetLink($request->only('email'));
        return response()->json([
            'message' => $status === Password::RESET_LINK_SENT
                ? 'Reset link sent to your email.'
                : 'Unable to send reset link.',
        ]);
    }
}

(2) التحقق من البريد الإلكتروني

PHP
// يجب أن يطبق النموذج MustVerifyEmail
class User extends Model implements MustVerifyEmail
{
    use Notifiable, VerifiesEmails;
}

// مسارات محمية — المستخدمون المتحققون فقط
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// التحقق من البريد الإلكتروني عبر API
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/email/verification-notification', function (Request $request) {
        $request->user()->sendEmailVerificationNotification();
        return response()->json(['message' => 'Verification link sent.']);
    });
});

(1) ▶ مثال: الإعداد الكامل لعملية مصادقة ShopMetrics

PHP
// bootstrap/app.php — إعداد وسطاء المصادقة
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tenant.resolve' => TenantResolve::class,
        'ability' => CheckAbilities::class,
    ]);
})

// نموذج المستخدم مع التحقق
class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'tenant_id', 'name', 'email', 'password', 'role', 'email_verified_at',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: نظام المصادقة الكامل لـ ShopMetrics

PHP
// ============================================
// شامل: نظام مصادقة ShopMetrics
// يغطي: مصادقة SPA، مصادقة بالرموز، القدرات، عزل المستأجرين
// ============================================

// routes/api.php — مسارات مصادقة API الكاملة
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
    Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => $r->user()->load('tenant'));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
        Route::post('/email/verification', function (Request $request) {
            $request->user()->sendEmailVerificationNotification();
            return response()->json(['message' => 'Verification email sent.']);
        });
        Route::post('/tokens', [Auth\TokenController::class, 'store']);
        Route::get('/tokens', fn (Request $r) => $r->user()->tokens);
        Route::delete('/tokens/{id}', fn (Request $r, $id) => $r->user()->tokens()->where('id', $id)->delete());
    });
});

// مسارات API المحمية
Route::middleware(['auth:sanctum', 'verified', 'tenant.resolve'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\ShopController::class);
        Route::apiResource('products', Api\ProductController::class);
        Route::apiResource('orders', Api\OrderController::class)->only(['index', 'show', 'update']);
        Route::get('analytics/overview', [Api\AnalyticsController::class, 'overview'])
            ->middleware('ability:read-analytics');
        Route::post('reports/generate', [Api\ReportController::class, 'generate'])
            ->middleware('ability:write-reports');
    });

❓ أسئلة شائعة

س ما الفرق بين Sanctum و Passport؟
ج Sanctum خفيف ويدعم جلسات SPA ومصادقة بسيطة بالرموز؛ Passport يوفر تطبيقاً كاملاً لـ OAuth2 (رمز التفويض، ضمني، بيانات اعتماد العميل، إلخ). Sanctum كافٍ لمعظم التطبيقات؛ استخدم Passport فقط إذا كنت بحاجة إلى OAuth 2.0.
س أين تُخزن الرموز في وضع SPA؟
ج وضع SPA لا يستخدم الرموز! يستخدم ملفات تعريف الارتباط/الجلسات للمصادقة، تماماً مثل تطبيقات الويب التقليدية. تحتاج الواجهة الأمامية فقط إلى تعيين withCredentials: true، ويتعامل Sanctum تلقائياً مع CSRF والجلسات.
س أين يجب تخزين الرموز في الواجهة الأمامية؟
ج على أجهزة الموبايل، خزّنها في Keychain أو Keystore؛ لـ SPAs التي تستخدم وضع الرموز، خزّنها في localStorage (يحمل خطر XSS) أو في ملف تعريف ارتباط HttpOnly. أفضل ممارسة: استخدم وضع الجلسة لـ SPAs لتجنب تخزين الرموز.
س كيف أحدد وقت انتهاء الرمز؟
ج عيّن expiration (بالدقائق) في config/sanctum.php، أو مرر المعامل الثالث، $user->createToken('name', ['*'], now()->addDays(30))، عند إنشاء الرمز. الرموز المنتهية تُبطل تلقائياً.
س كيف يمنع نظام المستأجرين المتعدد استخدام الرموز عبر المستأجرين؟
ج في وسيط TenantResolve، تحقق من أن tenant_id للمستخدم الذي ينتمي إليه الرمز يتطابق مع مستأجر الطلب. بدلاً من ذلك، يمكن ترميز معلومات المستأجر في قدرات الرمز.
س كيف أُبطِل جميع الرموز؟
ج $user->tokens()->delete() يُبطل جميع رموز المستخدم؛ $user->currentAccessToken()->delete() يُبطل الرمز الحالي فقط.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): إعداد مصادقة Sanctum SPA، تنفيذ ثلاث نقاط نهاية API للتسجيل وتسجيل الدخول وتسجيل الخروج، واستخدام Postman لاختبار عملية مصادقة الجلسة.

  2. تمرين متقدم (⭐⭐): تنفيذ نموذج المصادقة القائم على الرموز، إنشاء رمز بقدرات (abilities)، وكتابة وسيط للتحقق من صلاحيات tokenCan() لضمان أن دور "المحلل" يمكنه فقط قراءة البيانات.

  3. تحدي (⭐⭐⭐): تنفيذ عزل المستأجرين المتعددين باستخدام نطاق TenantScope ووسيط TenantResolve لضمان أن رموز API يمكنها فقط الوصول إلى بيانات مستأجرها الخاص، وأن الطلبات عبر المستأجرين تُرجع خطأ 403.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%