📍 الدليل المرجعي: Laravel 11 وPHP 8.4
الاستيثاق هو طبقة الأمن الأكثر حرجًا في أيّ تطبيق ويب. Laravel 11 يعرض Laravel Sanctum للـ APIs والـ SPAs — حلّ خفيف يُصدر tokens API محمولة أو يستند إلى cookies الجلسة. يُغطّي هذا الدليل وضعَي Sanctum: tokens API للعملاء المحمولين والخدمات الخارجية، والاستيثاق SPA لواجهات Vue.js وReact وNuxt على نفس النطاق.
المتطلّبات
- مشروع Laravel 11.
- API REST في مكانه.
- جدول users بـ migrations الافتراضية.
- الوقت: 45 دقيقة.
الخطوة 1 — تثبيت Sanctum وتحضير القاعدة
في Laravel 11، Sanctum يُثَبَّت عبر install:api الذي يُهَيِّئ routing API في الوقت ذاته.
php artisan install:api
هذا الأمر يُثَبِّت laravel/sanctum، ينشر config/sanctum.php، ينشر migration التي تُنشئ جدول personal_access_tokens. هذا الجدول يُخزّن كلّ tokens المُصدَرة: اسمها، hash SHA-256 لقيمتها، abilities المُرافقة، وتاريخ انتهاء اختياري.
php artisan migrate:status
الخطوة 2 — تهيئة نموذج User بـ HasApiTokens
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed', // hash تلقائي (Laravel 10+)
];
}
الـ cast 'password' => 'hashed' ميزة مُقَدَّمة في Laravel 10 تُهَشِّر كلمة السرّ تلقائيًّا عند الإسناد — لا حاجة لـ bcrypt() أو Hash::make() يدوي.
الخطوة 3 — إنشاء controller الاستيثاق
php artisan make:controller Auth/AuthController
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
// POST /api/register
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$user = User::create($validated);
$token = $user->createToken('api-token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
], 201);
}
// POST /api/login
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
'device_name' => ['required', 'string', 'max:100'],
]);
if (! Auth::attempt($request->only('email', 'password'))) {
throw ValidationException::withMessages([
'email' => ['Les identifiants fournis sont incorrects.'],
]);
}
$user = Auth::user();
$token = $user->createToken($request->device_name)->plainTextToken;
return response()->json(['token' => $token]);
}
// POST /api/logout
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Déconnecté avec succès.']);
}
}
device_name هي ممارسة Sanctum الجيّدة لتسمية tokens حسب الجهاز (« iPhone 15 لـ Salim »، « Chrome Windows »). تُتيح للمستخدم إلغاء tokens حسب الجهاز.
الخطوة 4 — إعلان routes الاستيثاق
<?php
// routes/api.php
use App\Http\Controllers\Auth\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// Routes عامّة
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Routes محمية
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', fn (Request $request) => $request->user());
});
الخطوة 5 — اختبار endpoints الاستيثاق
# 1. التسجيل
curl -s -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"name":"Salim Al-Otaibi","email":"salim@example.com","password":"secret12","password_confirmation":"secret12"}' | jq .
# المتوقّع: {"user":{...}, "token":"1|AbCdEfGhIj..."}
# 2. الاتّصال واسترداد token
TOKEN=$(curl -s -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"email":"salim@example.com","password":"secret12","device_name":"curl-test"}' | jq -r '.token')
echo "Token: $TOKEN"
# 3. الوصول إلى route محمية
curl -s http://localhost:8000/api/user \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json" | jq .
إشارة النجاح: الأمر الثالث يُرجع JSON للمستخدم المُتَّصَل. إن حصلت على {"message":"Unauthenticated."}، تحقّق من إرسال header Authorization: Bearer TOKEN ومن تطبيق middleware auth:sanctum.
الخطوة 6 — Token abilities: صلاحيات دقيقة
<?php
// إصدار token مع abilities محدّدة
$token = $user->createToken('mobile-app', ['articles:read', 'articles:create'])->plainTextToken;
// التحقّق من abilities في controller أو middleware
public function store(Request $request): JsonResponse
{
if (! $request->user()->tokenCan('articles:create')) {
return response()->json(['error' => 'Permission insuffisante.'], 403);
}
// ...
}
// إصدار token مع انتهاء (مثلًا 24 ساعة)
$token = $user->createToken(
'temp-access',
['reports:read'],
now()->addHours(24)
)->plainTextToken;
// جَدوَلَة حذف tokens المنتهية (routes/console.php)
Schedule::command('sanctum:prune-expired --hours=24')->daily();
الأمر sanctum:prune-expired يحذف من personal_access_tokens tokens المنتهية. دون هذا الأمر المُجَدوَل، الجدول ينمو بلا حدود.
أخطاء شائعة
| الخطأ | السبب | الحلّ |
|---|---|---|
Unauthenticated. رغم token صحيح |
header Accept: application/json غائب |
أرسل دائمًا Accept: application/json |
| Token موجود لكن abilities خاطئة | Token مُنشأ دون abilities | استعمل tokenCan() وتحقّق من abilities عند الإصدار |
جدول personal_access_tokens غائب |
install:api لم يُنَفَّذ |
شَغِّل php artisan migrate |
| كلمة السرّ غير مُهَشَّرة | cast 'password' => 'hashed' غائب |
أضف cast أو استعمل Hash::make() |
FAQ
الفرق بين Sanctum وPassport؟ Passport يُنَفِّذ بروتوكول OAuth2 الكامل (authorization code، client credentials) — لتكاملات معقّدة. Sanctum أبسط ويُغَطّي 90% من الاحتياجات: tokens API وSPA على نفس النطاق.
كيف نُلغي كلّ tokens مستخدم؟ $user->tokens()->delete(). لإلغاء token الحالي فقط: $request->user()->currentAccessToken()->delete().
أيمكن تحديد عدد tokens نشطة لكلّ مستخدم؟ ليس أصليًّا. يجب تنفيذ المنطق يدويًّا: قبل إصدار token جديد، عُدّ الموجودة واحذف الأقدم إن تجاوزت الحدّ.