تخطَّ إلى المحتوى

الـ Middleware وتسجيل الدخول في Laravel

الدرس 9 من 11 · Laravel

مدونتك تعرض المقالات وتحفظها، لكن أي زائر يستطيع الآن أن يفتح صفحة «مقال جديد» ويكتب ما يشاء! في هذا الدرس ستتعلم كيف تحمي صفحاتك: ستبني تسجيل دخول وخروج، وتغلق الصفحات الخاصة أمام الزوار، وتكتب حارساً خاصاً لصفحات المدير.

الـ Middleware (الوسيط) كود يقف بين الطلب والمتحكم: يفحص كل طلب قبل أن يصل، ثم يقرر: هل يمر، أم يُرفض، أم يُحوَّل إلى مكان آخر؟

تخيّل بوابات الأمن في المطار: قبل أن تصل إلى الطائرة تمر على فحص التذكرة، ثم فحص الجواز، ثم فحص الحقائب. كل بوابة تتأكد من شيء واحد، ومن يفشل في واحدة لا يكمل.

Laravel يشغّل وسطاء كثيرين دون أن تشعر: الفحص الذي يرفض النماذج بلا @csrf (من الدرس السادس) وسيط، وتشغيل الجلسات وسيط. وأشهر وسيط ستستخدمه بنفسك اسمه auth: يسمح بالمرور لمن سجّل دخوله فقط.

Laravel يأتي ومعه جدول users و Model اسمه User جاهزان. لننشئ مستخدماً من الطرفية بالأمر php artisan tinker:

User::create([
'name' => 'سارة',
'email' => '[email protected]',
'password' => 'secret123',
]);

افتح قاعدة البيانات وانظر إلى عمود password: لن تجد secret123، بل نصاً طويلاً يبدأ بـ $2y$. الـ Model يشفّر كلمة السر تلقائياً قبل حفظها، لأن فيه الإعداد 'password' => 'hashed'. وهذا التشفير باتجاه واحد: لا أحد يستطيع استرجاع الكلمة الأصلية، حتى أنت.

routes/web.php
use App\Http\Controllers\AuthController;
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');

اسم المسار login مهم: الوسيط auth يبحث عن مسار بهذا الاسم ليحوّل إليه الزوار.

أنشئ المتحكم بـ php artisan make:controller AuthController واكتب فيه:

app/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function showLogin()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
return back()
->withErrors(['email' => 'البريد أو كلمة السر غير صحيحة'])
->onlyInput('email');
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}

لنفهم الدالة login:

  • Auth::attempt تبحث عن مستخدم بهذا البريد، وتقارن كلمة السر بالنسخة المشفّرة. إذا تطابقتا تسجّل دخوله وتعيد true.
  • session()->regenerate() تغيّر معرّف الجلسة بعد الدخول، للحماية من هجوم تثبيت الجلسة.
  • redirect()->intended('/dashboard') لمسة ذكية: إذا كان الزائر قد حاول فتح /profile فحُوِّل إلى صفحة الدخول، فستعيده بعد الدخول إلى /profile التي أرادها. وإن لم يكن، فإلى /dashboard.
  • عند الفشل نعود برسالة خطأ، و onlyInput('email') تحتفظ بالبريد المكتوب دون كلمة السر.

والنموذج قالب Blade عادي، كما تعلمت في الدرس السادس:

resources/views/auth/login.blade.php
<form method="POST" action="/login">
@csrf
<label>البريد الإلكتروني</label>
<input type="email" name="email" value="{{ old('email') }}">
@error('email')
<p class="error">{{ $message }}</p>
@enderror
<label>كلمة السر</label>
<input type="password" name="password">
<button type="submit">دخول</button>
</form>

الآن نضع الصفحات الخاصة داخل مجموعة عليها الوسيط auth:

routes/web.php
Route::middleware('auth')->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/profile', function () {
return 'ملف ' . auth()->user()->name;
});
});

النتيجة عند التجربة:

الحالة ما يحدث
زائر يفتح /dashboard يُحوَّل إلى /login
دخول بكلمة سر خاطئة يعود للنموذج ومعه رسالة الخطأ
دخول ببيانات صحيحة يُحوَّل إلى /dashboard
مستخدم مسجّل يفتح /profile يرى «ملف سارة»
بعد الخروج تُغلق الصفحات من جديد

الدالة auth()->user() تعطيك كائن المستخدم الحالي في أي مكان. داخل المجموعة المحمية تضمن أنه موجود.

كثيراً ما تريد عرض أشياء مختلفة للمستخدم المسجّل وللزائر، مثل زر الدخول أو الخروج:

resources/views/dashboard.blade.php
@auth
<p>أهلاً {{ auth()->user()->name }}</p>
<form method="POST" action="/logout">
@csrf
<button type="submit">تسجيل الخروج</button>
</form>
@endauth
@guest
<a href="/login">تسجيل الدخول</a>
@endguest

لاحظ أن الخروج نموذج POST وليس رابطاً عادياً، وفيه @csrf. لو كان رابط GET لاستطاع أي موقع خبيث أن يسجّل خروج زوارك بصورة مخفية.

Middleware خاص: صفحات المدير

Section titled “Middleware خاص: صفحات المدير”

الوسيط auth يسأل «هل سجّلت دخولك؟». لكن لوحة الإدارة تحتاج سؤالاً آخر: «هل أنت مدير؟». أضف أولاً عموداً للمستخدمين بملف Migration جديد:

$table->boolean('is_admin')->default(false);

ثم أنشئ الوسيط بالأمر php artisan make:middleware EnsureIsAdmin:

app/Http/Middleware/EnsureIsAdmin.php
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->is_admin) {
abort(403, 'هذه الصفحة للمدير فقط');
}
return $next($request);
}

كل وسيط له الدالة handle: إما أن يوقف الطلب (هنا بخطأ 403، ومعناه «ممنوع»)، وإما أن يستدعي $next($request) ليمرره إلى البوابة التالية. أعطِ الوسيط اسماً مختصراً:

bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureIsAdmin::class,
]);
})

واستخدمه مع auth:

routes/web.php
Route::get('/admin', function () {
return 'لوحة المدير';
})->middleware(['auth', 'admin']);

المستخدم العادي يرى صفحة خطأ 403 فيها رسالتنا، والمدير يرى «لوحة المدير». ولجعل مستخدم مديراً من tinker: $user->is_admin = true; $user->save();.

  1. انقل مسارات /posts/create و POST /posts إلى داخل مجموعة auth، وتأكد أن الزائر لا يستطيع إضافة مقال.
  2. أضف صفحة تسجيل حساب جديد: نموذج فيه الاسم والبريد وكلمة السر، ثم User::create(...) ثم Auth::login($user).
  3. أضف لجدول posts عمود user_id، واحفظ فيه auth()->id() عند إنشاء المقال، واعرض اسم الكاتب تحت العنوان بعلاقة belongsTo.
  • مسار الدخول بلا اسم login: الوسيط auth يبحث عن هذا الاسم، وبدونه يظهر الخطأ Route [login] not defined.
  • إضافة is_admin إلى القائمة المسموح ملؤها: عندها يستطيع أي مسجّل جديد أن يرسل is_admin=1 ويصبح مديراً! اتركه خارجها، وعيّنه يدوياً.
  • الخروج برابط GET: اجعله نموذج POST مع @csrf.
  • الاكتفاء بإخفاء الزر: إخفاء زر «حذف» بـ @auth لا يحمي المسار نفسه. الحماية الحقيقية بالوسيط على المسار.

سؤال

ماذا يحدث عندما يفتح زائر غير مسجّل مساراً عليه الوسيط auth؟

سؤال

ما دور $next($request) داخل الوسيط؟

  • الوسيط كود يفحص الطلب قبل وصوله إلى المتحكم.
  • Auth::attempt تتحقق من البيانات وتسجّل الدخول، و Auth::logout للخروج.
  • Route::middleware('auth')->group(...) تحمي مجموعة مسارات.
  • @auth و @guest و auth()->user() للتعامل مع المستخدم الحالي.
  • تكتب وسيطك الخاص بـ make:middleware، وتسجّل اسمه في bootstrap/app.php.