نظام المصادقة في Laravel—Sanctum
Sanctum هو "حارس البوابة" في Laravel—يتولى المصادقة القائمة على الجلسات لصفحات الويب والمصادقة القائمة على الرموز لواجهات API، مما يوفر نظامي مصادقة في إطار واحد.
1. ما ستتعلمه
- تثبيت وإعداد Laravel Sanctum: مصادقة SPA + مصادقة بالرموز
- عملية التسجيل/تسجيل الدخول/تسجيل الخروج: نهج القناتين (جلسة الويب + رمز API)
- قدرات الرموز والصلاحيات: تحكم دقيق عبر حقل "abilities"
- عزل المصادقة متعدد المستأجرين: وسيط ونطاق يدركان المستأجر
- عملية إعادة تعيين كلمة المرور والتحقق من البريد الإلكتروني
2. قصة حقيقية لمهندس أمن
(1) نقطة الألم: حوادث أمنية ناتجة عن ارتباك مخططات مصادقة API
استخدم Bob مصادقة JWT لواجهة ShopMetrics API والجلسات للويب—نظاما مصادقة منفصلان، مع عدم مزامنة حالات تسجيل دخول المستخدمين. بعد أن سجّلت Alice دخولها عبر المتصفح، كان لا يزال عليها الحصول على رمز منفصل لاستدعاءات API، الذي كان ينتهي بشكل متكرر، مما يتسبب في انقطاع العمليات. والأخطر من ذلك، اكتشف Charlie أن رمز API واحداً يمكنه الوصول إلى بيانات جميع المستأجرين—لم يكن هناك عزل بين المستأجرين، مما يشكل خطراً كبيراً لتسريب البيانات عبر المستأجرين.
(2) حل "Sanctum"
يوحّد Sanctum مصادقة الويب وAPI—في وضع SPA، يتشارك المتصفح وAPI الجلسة نفسها؛ وفي وضع الرموز، يوفر رموز API لعملاء الطرف الثالث؛ وحقل abilities يسمح بالتحكم الدقيق في الصلاحيات.
// مصادقة 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) التثبيت
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
# ينشئ: جدول personal_access_tokens
(2) الإعداد
// 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
// 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
# .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
الناتج:
# تم تنفيذ الأمر بنجاح
4. عملية التسجيل/تسجيل الدخول/تسجيل الخروج
(1) التسجيل
// 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) تسجيل الدخول
// 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) المصادقة وتسجيل الدخول بالرموز
// 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
// 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());
});
الناتج:
// تم التنفيذ بنجاح
5. قدرات الرموز والصلاحيات
(1) إنشاء رمز بقدرات (abilities)
// إنشاء رمز بقدرات محددة
$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) التحقق من قدرات الرمز
// في المتحكم أو الوسيط
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
// 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);
}
}
الناتج:
// تم التنفيذ بنجاح
6. عزل المصادقة متعدد المستأجرين
(1) مخطط تسلسل مصادقة رموز Sanctum
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) وسيط مصادقة المستأجر
// 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) نطاق المستأجر المتعدد
// 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 متعددة المستأجرين
// 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);
});
});
});
الناتج:
// تم التنفيذ بنجاح
7. إعادة تعيين كلمة المرور والتحقق من البريد الإلكتروني
(1) إعادة تعيين كلمة المرور
// 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) التحقق من البريد الإلكتروني
// يجب أن يطبق النموذج 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
// 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);
}
}
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: نظام المصادقة الكامل لـ ShopMetrics
// ============================================
// شامل: نظام مصادقة 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');
});
❓ أسئلة شائعة
withCredentials: true، ويتعامل Sanctum تلقائياً مع CSRF والجلسات.expiration (بالدقائق) في config/sanctum.php، أو مرر المعامل الثالث، $user->createToken('name', ['*'], now()->addDays(30))، عند إنشاء الرمز. الرموز المنتهية تُبطل تلقائياً.$user->tokens()->delete() يُبطل جميع رموز المستخدم؛ $user->currentAccessToken()->delete() يُبطل الرمز الحالي فقط.📖 ملخص
- Sanctum يوحّد آليتي المصادقة: جلسة الويب ورمز API
- وضع SPA يستخدم ملفات تعريف الارتباط/الجلسات للمصادقة، مما يلغي الحاجة لإدارة الرموز
- وضع الرموز يستخدم
plainTextTokenلتوفير وصول API لعملاء الطرف الثالث - قدرات الرمز (Abilities): تحكم دقيق في الصلاحيات لتجنب منح وصول مفرط
- وسيط TenantResolve + نطاق TenantScope لتنفيذ عزل المصادقة متعدد المستأجرين
- التحقق من البريد الإلكتروني وإعادة تعيين كلمة المرور جاهزان للاستخدام فوراً
📝 تمارين
-
تمرين أساسي (⭐): إعداد مصادقة Sanctum SPA، تنفيذ ثلاث نقاط نهاية API للتسجيل وتسجيل الدخول وتسجيل الخروج، واستخدام Postman لاختبار عملية مصادقة الجلسة.
-
تمرين متقدم (⭐⭐): تنفيذ نموذج المصادقة القائم على الرموز، إنشاء رمز بقدرات (abilities)، وكتابة وسيط للتحقق من صلاحيات
tokenCan()لضمان أن دور "المحلل" يمكنه فقط قراءة البيانات. -
تحدي (⭐⭐⭐): تنفيذ عزل المستأجرين المتعددين باستخدام نطاق TenantScope ووسيط TenantResolve لضمان أن رموز API يمكنها فقط الوصول إلى بيانات مستأجرها الخاص، وأن الطلبات عبر المستأجرين تُرجع خطأ 403.



