المصادقة و JWT — نظام هوية API آمن
JWT مثل مفتاح غرفة الفندق—Access Token هو تصريح يومي (صالح لفترة قصيرة)، و Refresh Token هو تصريح شهري (صالح لفترة طويلة ولكن يمكن إلغاؤه في أي وقت)؛ مكتب الاستقبال (خدمة المصادقة) مسؤول عن إصدار المفاتيح والتحقق منها.
1. ما ستتعلمه
- مبادئ وبنية JWT: شرح Header و Payload و Signature
- إصدار والتحقق من Access Tokens و Refresh Tokens (رمز مزدوج) باستخدام
python-jose - دمج
OAuth2PasswordBearerمع أدوات أمان FastAPI - تجزئة كلمات المرور: أفضل ممارسات
passlib+bcrypt - سيناريو Alice: مصادقة SaaS متعددة المستأجرين لـ PriceTracker—مصفوفة صلاحيات لخطط اشتراك مختلفة
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: واجهات API غير المصادق عليها يتم استخراجها بشكل خبيث
واجهة PriceTracker API الخاصة بـ Alice مفتوحة بالكامل، ويمكن لأي شخص استدعاء جميع نقاط النهاية. كتب منافس نصًا برمجيًا يستخرج مليون سجل بيانات أسعار كل دقيقة. لأسوأ من ذلك، استخدم شخص ما واجهة API لإرسال بيانات أسعار مزيفة، مما أفسد قاعدة البيانات بالكامل. تحتاج Alice إلى تنفيذ تسجيل المستخدم وتسجيل الدخول، ومصادقة API، والتحكم في الوصول بناءً على مستويات اشتراك مختلفة، لكنها لا تعرف كيف تفعل ذلك بأمان.
(2) حلول مصادقة JWT
JWT (JSON Web Token) هو حل مصادقة عديم الحالة: يحصل المستخدمون على رمز عند تسجيل الدخول، ويتضمنون الرمز في الطلبات اللاحقة، ويتحقق الخادم من التوقيع—لا حاجة لتخزين الجلسات، ويدعم الأنظمة الموزعة بطبيعته.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@app.get("/me")
async def get_me(token: str = Depends(oauth2_scheme)):
user = verify_token(token)
return user
(3) العائد
بمجرد تمكين مصادقة API، لن يتمكن المستخدمون غير المسجلين من الوصول إلى نقاط النهاية المحمية، وسيتم حظر الاستخراج الخبيث بواسطة كل من برمجية تحديد المعدل الوسيطة والمصادقة. يمكن للمستخدمين Pro استيراد البيانات بشكل مجمّع، بينما يقتصر المستخدمون المجانيون على 1000 سجل؛ المنافسون يمكنهم فقط عرض البيانات العامة.
3. مبادئ وبنية JWT
(1) البنية الثلاثية لـ JWT
يتكون JWT من ثلاثة أجزاء: Header (الخوارزمية)، Payload (البيانات)، و Signature، مفصولة بـ ..
sequenceDiagram
participant Client as Bob الواجهة الأمامية
participant Auth as /auth/login
participant API as واجهة API المحمية
participant Verify as مدقق الرمز
Client->>Auth: POST بريد إلكتروني + كلمة مرور
Auth->>Auth: التحقق من بيانات الاعتماد
Auth-->>Client: Access Token + Refresh Token
Client->>API: GET /products (Bearer Token)
API->>Verify: فك تشفير والتحقق من التوقيع
Verify-->>API: بيانات المستخدم
API-->>Client: 200 OK + البيانات
Note over Client,Auth: تنتهي صلاحية Access Token بعد 30 دقيقة
Client->>Auth: POST /auth/refresh (Refresh Token)
Auth-->>Client: Access Token جديد
| القسم | المحتوى | مثال |
|---|---|---|
| Header | نوع الخوارزمية | {"alg": "HS256", "typ": "JWT"} |
| Payload | بيانات المستخدم (Claims) | {"sub": "alice", "role": "admin", "exp": 1700000000} |
| Signature | التوقيع | HMACSHA256(header.payload, secret) |
(1) ▶ مثال: ترميز وفك تشفير JWT
from jose import jwt, JWTError
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise ValueError("Invalid token")
# اختبار
token = create_access_token({"sub": "alice", "role": "admin"})
print(f"Token: {token[:50]}...")
payload = verify_token(token)
print(f"Payload: {payload}")
الناتج:
# تم تعريف الدالة بنجاح
(2) Access Token مقابل Refresh Token
| البعد | Access Token | Refresh Token |
|---|---|---|
| فترة الصلاحية | 30 دقيقة | 7 أيام |
| الغرض | الوصول إلى موارد API | تحديث Access Token |
| التخزين | الذاكرة (الواجهة الأمامية) | HttpOnly Cookie |
| خطر التعرض | عالي (مضمن في كل طلب) | منخفض (يُستخدم فقط أثناء التحديث) |
| طريقة الإلغاء | انتظار انتهاء الصلاحية | القائمة السوداء للخادم |
4. دمج OAuth2PasswordBearer
(1) ▶ مثال: تكوين المصادقة الكامل
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE = timedelta(minutes=30)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
app = FastAPI()
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# في الإنتاج: استعلام المستخدم من قاعدة البيانات
user = {"username": username, "role": payload.get("role", "user")}
return user
@app.post("/auth/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# في الإنتاج: التحقق من كلمة المرور من قاعدة البيانات
if form_data.username != "alice" or form_data.password != "secret":
raise HTTPException(status_code=401, detail="Incorrect credentials")
access_token = create_access_token(
data={"sub": form_data.username, "role": "admin"},
expires_delta=ACCESS_TOKEN_EXPIRE,
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/me")
async def read_me(current_user: dict = Depends(get_current_user)):
return current_user
الناتج:
# تم تعريف الدالة بنجاح
5. تجزئة كلمات المرور
(1) passlib + bcrypt
لا تُخزن كلمات المرور أبدًا كنص عادي؛ تُملح وتُجزأ باستخدام خوارزمية bcrypt.
(1) ▶ مثال: تجزئة كلمة المرور والمصادقة
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
# اختبار
hashed = hash_password("my-secret-password")
print(f"Hashed: {hashed[:30]}...")
print(f"Verify correct: {verify_password('my-secret-password', hashed)}")
print(f"Verify wrong: {verify_password('wrong-password', hashed)}")
الناتج:
Hashed: $2b$12$K3YQ8z9wB5eF7gH1j...
Verify correct: True
Verify wrong: False
(2) أفضل ممارسات أمان كلمات المرور
| الممارسة | الوصف |
|---|---|
| استخدام bcrypt | تجزئة مملحة تكيفية لمنع جداول القوس القزح |
| لا يستخدم MD5/SHA256 | بدون ملح؛ الحسابات سريعة جدًا، مما يجعله عرضة لهجمات القوة الغاشمة |
| طول كلمة المرور ≥ 8 | فرض الحد الأدنى للطول |
| التجزئة قبل التحقق | منع هجمات التوقيت (مدمج في passlib) |
| SECRET_KEY طويل بما يكفي | سلسلة عشوائية من 32 حرفًا على الأقل؛ خزنها في متغير بيئة |
6. مصفوفة صلاحيات SaaS متعددة المستأجرين
(1) تصميم صلاحيات مستوى الاشتراك
(1) ▶ مثال: اعتمادات صلاحيات الاشتراك
from fastapi import Depends, HTTPException
SUBSCRIPTION_LIMITS = {
"free": {"max_import": 1000, "websocket": False, "export": False},
"pro": {"max_import": 100000, "websocket": True, "export": True},
"enterprise": {"max_import": 1000000, "websocket": True, "export": True},
}
def require_subscription(min_level: str):
LEVELS = {"free": 0, "pro": 1, "enterprise": 2}
async def check_subscription(user: dict = Depends(get_current_user)):
user_level = user.get("subscription", "free")
if LEVELS.get(user_level, 0) < LEVELS.get(min_level, 0):
raise HTTPException(
status_code=403,
detail=f"Requires {min_level} subscription. Current: {user_level}",
)
return user
return check_subscription
@app.get("/api/v1/analytics")
async def get_analytics(user=Depends(require_subscription("pro"))):
return {"total_products": 1000000, "active_users": 5000}
@app.post("/api/v1/prices/bulk")
async def bulk_import(
prices: list[PriceCreate],
user=Depends(require_subscription("free")),
):
limits = SUBSCRIPTION_LIMITS[user.get("subscription", "free")]
if len(prices) > limits["max_import"]:
raise HTTPException(
status_code=403,
detail=f"Import limit: {limits['max_import']} for {user['subscription']} plan",
)
return {"imported": len(prices)}
الناتج:
# تم تعريف الدالة بنجاح
| نقطة النهاية | مجاني | Pro | Enterprise |
|---|---|---|---|
| GET /products | 1,000/يوم | غير محدود | غير محدود |
| POST /prices | 1,000/دفعة | 100,000/دفعة | 1,000,000/دفعة |
| WebSocket /ws/prices | - | ✓ | ✓ |
| GET /analytics | - | ✓ | ✓ |
| تصدير CSV | - | ✓ | ✓ |
| تحديد معدل API | 100/دقيقة | 1,000/دقيقة | غير محدود |
❓ أسئلة شائعة
📖 ملخص
- يتكون JWT من ثلاثة أجزاء: Header و Payload و Signature. المصادقة عديمة الحالة تدعم الأنظمة الموزعة بطبيعتها
- Access Token (قصير المدى) يُستخدم للوصول إلى API، بينما Refresh Token (طويل المدى) يُستخدم للتحديث؛ نظام الرمز المزدوج هذا يعزز الأمان
OAuth2PasswordBearerيدمج أدوات أمان FastAPI؛ Swagger UI يعرض نموذج تسجيل الدخول تلقائيًا- تُخزن كلمات المرور كتجزئات bcrypt؛ passlib يتعامل مع منطق التملح والتحقق
- مصفوفة صلاحيات الاشتراك ثلاثية المستويات لـ PriceTracker: Free/Pro/Enterprise، تُنفذ عبر
require_subscriptionDI
📝 تمارين
- تمرين أساسي (الصعوبة ⭐): نفذ نقطة نهاية
/auth/loginلاستقبال اسم المستخدم وكلمة المرور؛ بعد المصادقة، أعد JWT Access Token (صالح لمدة 30 دقيقة). استخدمDepends(oauth2_scheme)لتحليل المستخدم في نقطة النهاية/me. تلميح:OAuth2PasswordRequestForm+jwt.encode - تمرين متقدم (الصعوبة ⭐⭐): أضف آلية رمز التحديث. Access Token صالح لمدة 15 دقيقة، و Refresh Token صالح لمدة 7 أيام. نفذ نقطة نهاية
/auth/refreshلاستبدال رمز التحديث بـ Access Token جديد. تلميح: دالتانcreate_token+ أوقات انتهاء صلاحية مختلفة - تحدي (الصعوبة ⭐⭐⭐): نفذ نظام تسجيل/دخول/صلاحيات كامل—خزن كلمات المرور لنقطة نهاية التسجيل باستخدام تجزئة bcrypt؛ استخدم سلسلة DI
require_subscription(min_level)للتحقق من مستويات الاشتراك؛ قيد المستخدمين المجانيين باستيراد 1000 سجل، بينما المستخدمون Pro بدون حد. تلميح:hash_password+verify_password+require_subscriptionDI
---|



