🎓 מפתחי בינה מלאכותית - GenAI Engineers

הפרדה בין צד לקוח וצד שרת, ושילוב APIs בין שירותים

234 שעות אקדמיות 2 סמסטרים E2E - קונספט עד ענן

Authentication ו-Authorization ב-API - מדריך פרקטי

זה הפרק שמבדיל בין “שרת שמחזיר JSON” לבין מערכת אמיתית. נבנה מודל מנטלי ברור, נבחר שיטה מתאימה, ונראה איך מיישמים ב-FastAPI בצורה סטנדרטית.

הגדרות קצרות:
Authentication = מי אתה? (זיהוי משתמש)
Authorization = מה מותר לך? (הרשאות לפעולות/משאבים)

1) מודל מנטלי: בקשה מאובטחת ב-HTTP

ב-HTTP אין “משתמש מחובר” באופן מובנה. כל בקשה עומדת בפני עצמה. לכן אנחנו מוסיפים מזהה זהות לכל בקשה באחת מהצורות הבאות:

2) השיטות הנפוצות והטרייד-אופים

API Key

מפתח סטטי שמזהה לקוח/אפליקציה (לא תמיד משתמש). מתאים לסקריפטים, אינטגרציות, שירות-לשירות.

  • יתרון: פשוט ליישום
  • חסרון: ניהול רוטציה, אין “הרשאות משתמש” טבעי

Session Cookie

שרת מחזיק session store (או חתימה) והלקוח מקבל cookie.

  • יתרון: נוח לאפליקציות web קלאסיות
  • חסרון: דורש state (או פתרון מבוזר), צריך CSRF ב-cookies

JWT (Bearer token)

Token חתום שמכיל claims (למשל user_id, roles). נשלח בכל בקשה.

  • יתרון: stateless יחסית, מתאים ל-API
  • חסרון: ביטול token קשה בלי blacklist/rotation

OAuth2 / OIDC

סטנדרט של התחברות דרך ספק זהות (Google/GitHub/IdP). מתאים כשיש multiple apps או SSO.

  • יתרון: תעשייתי, סקייל, SSO
  • חסרון: מורכב יותר, דורש flow נכון

במערכת מבוססת Cloud Run: JWT/API-keys מתחברים טבעי לסטטלס. Session cookies אפשר, אבל לרוב דורש Redis/DB ל-sessions (או פתרון חכם).

3) עקרונות אבטחה שלא מדלגים עליהם

4) יישום ב-FastAPI: Password hashing + JWT + protected routes

הדוגמה הבאה היא “שלד נכון” - קצר, אבל עם החלטות טובות: hash לסיסמה, יצירת JWT, והגנה על endpoints דרך dependency.

pip install fastapi uvicorn passlib[bcrypt] python-jose[cryptography]

4.1 Hash לסיסמה

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(password: str, password_hash: str) -> bool:
    return pwd_context.verify(password, password_hash)

4.2 JWT: יצירה ופענוח

from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError

JWT_SECRET = "CHANGE_ME"  # בפרודקשן מגיע מ-ENV
JWT_ALG = "HS256"
ACCESS_TOKEN_TTL_MIN = 30

def create_access_token(sub: str, roles: list[str]) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": sub,              # subject: user id
        "roles": roles,          # claims: roles
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(minutes=ACCESS_TOKEN_TTL_MIN)).timestamp()),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG)

def decode_token(token: str) -> dict:
    try:
        return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG])
    except JWTError:
        raise ValueError("Invalid token")

4.3 FastAPI Dependencies: “מי המשתמש?”

FastAPI עובד נהדר עם dependencies: פונקציה שמחלצת משתמש מה-token, ואז endpoints מוגנים פשוט משתמשים בה.

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(payload: dict = Depends(oauth2_scheme)):
    # oauth2_scheme מחזיר token string בפועל, אז נחלץ ונפענח
    # (נכתוב wrapper קטן כדי לשמור על קריאות)
    raise NotImplementedError
גרסה מלאה של get_current_user (קצרה ומעשית)
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    try:
        payload = decode_token(token)
        return {
            "user_id": payload.get("sub"),
            "roles": payload.get("roles", []),
        }
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token",
            headers={"WWW-Authenticate": "Bearer"},
        )

4.4 הגנה על Endpoint + בדיקת Role

def require_role(role: str):
    def checker(user: dict = Depends(get_current_user)):
        roles = set(user.get("roles", []))
        if role not in roles:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return checker

@app.get("/me")
def read_me(user: dict = Depends(get_current_user)):
    return {"id": user["user_id"], "roles": user["roles"]}

@app.delete("/admin/users/{user_id}")
def delete_user(user_id: str, admin: dict = Depends(require_role("admin"))):
    return {"status": "ok", "deleted": user_id}

5) Token lifecycle: Access + Refresh (למה זה חשוב)

Access token קצר חיים (למשל 15-30 דקות). Refresh token ארוך חיים (ימים/שבועות) ומשמש לקבלת access חדש. כך אם token נגנב, הנזק מוגבל.

מינימום מומלץ

  • Access token TTL קצר
  • Refresh token עם rotation
  • אחסון refresh בצורה בטוחה (HTTP-only cookie או storage מאובטח)

מה לא לעשות

  • לשים refresh token ב-localStorage ללא סיבה
  • TTL אינסופי ל-access token
  • להדפיס token ללוגים

6) Service-to-Service Auth (בענן)

כששירות אחד מדבר עם שירות אחר, לרוב לא משתמשים בסיסמאות משתמש. פתרונות תעשייתיים:

7) תרגול

שאלות בדיקה (10)
  1. מה ההבדל בין Authentication ל-Authorization?
  2. למה JWT מתאים למערכת stateless כמו Cloud Run?
  3. למה לא שומרים סיסמאות כפי שהן?
  4. מה הסיכון בלשים token ב-query param?
  5. מה היתרון ב-Access token קצר חיים?
  6. מה זה refresh token rotation ולמה הוא חשוב?
  7. מה ההבדל בין API key ל-JWT מבחינת “זהות”?
  8. מה ההבדל בין 401 ל-403?
  9. מתי תעדיף OAuth2/OIDC על JWT פנימי?
  10. איך Redis יכול לעזור ב-rate limiting?
משימת בית (מיני-פרויקט)

מטרה: להוסיף אימות והרשאות לפרויקט ה-CRUD שלך.

  • צור endpoint POST /auth/register שמקבל email+password ושומר user (עם password_hash).
  • צור endpoint POST /auth/login שמחזיר access token (JWT) אם הסיסמה נכונה.
  • הגן על GET /notes כך שיחזיר רק notes של המשתמש המחובר.
  • הוסף role admin ו-endpoint אחד שמותר רק לאדמין.
  • הוסף rate limiting בסיסי ל-login (אפשר עם Redis או בזיכרון עבור תרגול).

הערה: בפרודקשן secrets לא בקוד - רק ב-ENV.

בדיקת פתרון (Checklist)
  • סיסמאות נשמרות כ-hash בלבד.
  • token נשלח ב-Authorization Bearer header.
  • 401 כשאין/לא תקין token, 403 כשאין הרשאה.
  • יש הפרדה בין “מי המשתמש” לבין “מה מותר לו”.
  • יש TTL ברור ל-access token (וגם הסבר ל-refresh אם הוספת).
  • הוספת מנגנון בסיסי נגד brute-force (rate limit / cooldown).