المرحلة 3: نقاط نهاية API والمصادقة

تصميم RESTful ومصادقة JWT

3 دقيقة للقراءة

في هذه المرحلة نربط الواجهة العامة لـ TaskFlow: نقاط نهاية API تتبع اصطلاحات REST، مؤمّنة بتوكنات JWT ومحمية بالتحكم بالوصول حسب الأدوار.

هيكل عناوين URL في REST

واجهة REST نظيفة تربط الموارد بعناوين URL وتستخدم طرق HTTP للتعبير عن القصد:

الطريقةنمط العنوانالغرضرمز الحالة
POST/api/v1/projectsإنشاء مشروع201 Created
GET/api/v1/projectsسرد المشاريع200 OK
GET/api/v1/projects/{id}الحصول على مشروع واحد200 OK
PUT/api/v1/projects/{id}تحديث مشروع200 OK
DELETE/api/v1/projects/{id}حذف مشروع204 No Content
POST/api/v1/projects/{id}/tasksإنشاء مهمة في مشروع201 Created

قواعد عامة:

  • استخدم أسماء الجمع للمجموعات (/projects وليس /project).
  • ضع الموارد الفرعية تحت الأصلية (/projects/{id}/tasks).
  • أضف إصدارًا لواجهتك البرمجية (/api/v1/) حتى لا تفاجئ التغييرات الجذرية العملاء.
  • أرجع رمز الحالة الصحيح — وهذا قرار لا عملية بحث في جدول:

أي رمز حالة تستحقه هذه الاستجابة؟

هل نجح الطلب؟

الفرعان اللذان يخلطهما الناس هما 401 مقابل 403، و403 مقابل 404. وضبطهما هو ما يجعل واجهتك قابلة للتشخيص من شخص لا يستطيع قراءة سجلاتك.

تدفق مصادقة JWT

يستخدم TaskFlow توكنات JSON Web Tokens (JWT) للمصادقة عديمة الحالة:

التسجيل وتسجيل الدخول واستخدام التوكن

الخطوة المتحركة هي ما يجعل JWT عديم الحالة — وهي نفسها ما يجعل الإبطال صعباً.

الحساب موجودإرجاع التوكنصالح401 — أعد المحاولة بتوكن جديدPOST /auth/registerتُجزَّأ كلمة المرور بـ Argon2 و…POST /auth/loginتُفحَص بيانات الاعتماد مرة واحد…يقرأها أي أحدالخادم يوقّع توكن JWTالحمولة تحمل sub (معرّف المستخد…العميل يرسل Authorization…يسافر التوكن مع كل طلب لاحقالتحقق من التوقيع داخل ال…بلا رحلة إلى قاعدة البيانات. هذ…المعالج يعمل بهوية ذلك ال…‏get_current_user حسم الهوية قب…

يتكون JWT من ثلاثة أجزاء مفصولة بنقاط: header.payload.signature. يوقّع الخادم التوكن بمفتاح سري؛ وفي كل طلب يتحقق من التوقيع بدون الرجوع لقاعدة البيانات.

هذه الجملة الأخيرة هي كل جاذبية التصميم، وهي ثمنه أيضاً. فلأن شيئاً لا يُستعلَم عنه، لا يمكن إبطال شيء. يبقى التوكن صالحاً حتى يمر وقت exp، ولو حذفت المستخدم أو غيّرت كلمة مروره أو خفّضت صلاحيته في منتصف الجلسة. ومدة الثلاثين دقيقة في الكود أدناه ليست رقماً اعتباطياً — إنها حجم النافذة التي يظل فيها توكن مسروق أو قديم صالحاً للعمل. توكنات وصول قصيرة العمر مع توكن تحديث مُخزَّن على حدة هي كيف تستعيد أنظمة الإنتاج قدرة الإبطال؛ وإن كنت تحتاج إبطالاً فورياً فأنت تحتاج حالة على الخادم، وJWT وحده لن يمنحك إياها.

ولاحظ الشارة الكهرمانية: الحمولة مُرمَّزة بـ base64url لا مُشفَّرة. من يملك التوكن يستطيع قراءتها. فلا تضع في حمولة JWT شيئاً لا ترضى بتسليمه للعميل في صورة JSON صريح.

from datetime import datetime, timedelta, timezone
import jwt
from jwt.exceptions import InvalidTokenError

SECRET_KEY = "your-secret-key"  # يُحمّل من متغير البيئة
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict) -> str:
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

لماذا PyJWT، ولماذا تستحق عبارة «هذه المكتبة ميتة» التحقق

يستخدم TaskFlow مكتبة PyJWT، وهي نفسها ما يستخدمه درس الأمان في توثيق FastAPI. وستصادف كثيراً من الدروس الأقدم مبنيةً على python-jose، وكثيراً من النصائح التي تقول إنها مهجورة.

تلك النصيحة قديمة، والتحقق منها يستغرق نحو عشر ثوانٍ. أصدرت python-jose النسخة 3.4.0 في فبراير 2025 والنسخة 3.5.0 في مايو 2025، ويمكنك رؤية ذلك بنفسك في سجل إصداراتها على PyPI. فتكرار «مهجورة منذ 2021» في 2026 خطأ بكل بساطة.

ومع ذلك يبقى سبب حقيقي لمعرفة إصدار 3.4.0، وهو أنفع من قصة الهجر: فقد عالج الثغرة CVE-2024-33663، وهي ثغرة خلط الخوارزميات التي تصيب الإصدارات حتى 3.3.0. وخلط الخوارزميات هو صنف ثغرات JWT الجدير بأن تحمله معك: إذا ترك المُتحقِّق للتوكن نفسه أن يقرر خوارزمية التحقق، أمكن للمهاجم أن يقدّم توكناً ترويسته تقول HS256، موقَّعاً بمفتاحك العام RSA بوصفه سرّ HMAC — فيقبله المُتحقِّق.

والدفاع وسيط واحد، وهو موجود بالفعل في الكود أعلاه وأدناه:

payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

algorithms=[...] ليست حشواً. إنها السطر الذي يقول: الخادم هو من يقرر الخوارزمية المقبولة، لا التوكن. لا تحذفها أبداً، ولا تبنِ قائمتها من أي شيء وصل ضمن الطلب.

والعادة تتجاوز هذه المكتبة الواحدة: قبل أن تردد أن حزمة ما ميتة، افتح صفحتها في السجل وانظر إلى التواريخ. «مهجورة» ادعاء عن حال العالم، وله تاريخ صلاحية.

تجزئة كلمات المرور مع pwdlib + Argon2

توثيق FastAPI الحالي يوصي بـ pwdlib مع محرك Argon2 بدلاً من تركيبة passlib/bcrypt القديمة. Argon2 هو الفائز في مسابقة تجزئة كلمات المرور ومقاوم لهجمات القوة الغاشمة المعتمدة على GPU:

from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher

password_hash = PasswordHash((Argon2Hasher(),))

hashed = password_hash.hash("user-password")           # تجزئة
is_valid = password_hash.verify("user-password", hashed) # تحقق

حقن التبعيات في FastAPI للمصادقة

نظام Depends() في FastAPI يتيح لك حقن المستخدم الحالي في أي نقطة نهاية. التبعية تقرأ ترويسة Authorization: Bearer <token>، تفك تشفير JWT، تبحث عن المستخدم في قاعدة البيانات، وترجع كائن المستخدم (أو ترفع خطأ 401 Unauthorized):

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
from jwt.exceptions import InvalidTokenError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    except InvalidTokenError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    user = await db.get(User, int(user_id))
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return user

أي نقطة نهاية تحتاج مصادقة تضيف ببساطة current_user: User = Depends(get_current_user) لتوقيعها. FastAPI يتكفل بالباقي.

التحكم بالوصول حسب الأدوار (RBAC)

في TaskFlow، كل مستخدم له دور داخل المشروع (مالك أو مدير أو عضو). فحوصات الوصول تتم من خلال تبعية أخرى:

from enum import Enum

class ProjectRole(str, Enum):
    OWNER = "owner"
    ADMIN = "admin"
    MEMBER = "member"

def require_project_role(*allowed_roles: ProjectRole):
    async def checker(
        project_id: int,
        current_user: User = Depends(get_current_user),
        db: AsyncSession = Depends(get_db),
    ) -> ProjectMember:
        member = await get_project_member(db, project_id, current_user.id)
        if member is None or member.role not in allowed_roles:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
        return member
    return checker

الاستخدام في نقطة نهاية:

@router.put("/projects/{project_id}")
async def update_project(
    project_id: int,
    data: ProjectUpdate,
    member: ProjectMember = Depends(
        require_project_role(ProjectRole.OWNER, ProjectRole.ADMIN)
    ),
    db: AsyncSession = Depends(get_db),
):
    # فقط المالك أو المدير يصل لهذا السطر
    ...

نمط التصفح بالصفحات

لنقاط نهاية القوائم، يستخدم TaskFlow تصفح page/size مع العدد الإجمالي لتمكين الواجهة الأمامية من عرض أدوات التنقل:

from pydantic import BaseModel
from typing import Generic, TypeVar, Sequence

T = TypeVar("T")

class PaginatedResponse(BaseModel, Generic[T]):
    items: Sequence[T]
    total: int
    page: int
    size: int
    pages: int

معلمات الاستعلام ?page=1&size=20 تحرك حساب الإزاحة: offset = (page - 1) * size. الاستجابة تتضمن دائمًا total (العدد الكامل) و pages (العدد الإجمالي للصفحات).

توثيق OpenAPI التلقائي

FastAPI يولّد توثيق API تفاعلي من الكود بدون جهد إضافي:

  • Swagger UI على /docs -- اختبر نقاط النهاية مباشرة في المتصفح.
  • ReDoc على /redoc -- مرجع نظيف وسهل القراءة.

نماذج Pydantic v2 ورموز حالة الاستجابة والمصادقة المحقونة بالتبعيات تظهر جميعها تلقائيًا في التوثيق المولّد. يمكن للعملاء تصدير JSON الخاص بـ OpenAPI من /openapi.json لتوليد SDKs بأي لغة.


التالي: معمل عملي -- ستبني جميع نقاط نهاية المصادقة والمشاريع والمهام لـ TaskFlow. :::

اختبار

اختبار الوحدة 3: نقاط نهاية API والمصادقة

خذ الاختبار
هل كان هذا الدرس مفيدًا؟

سجّل الدخول للتقييم