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"},
)
Authentication ו-Authorization ב-API - מדריך פרקטי
זה הפרק שמבדיל בין “שרת שמחזיר JSON” לבין מערכת אמיתית. נבנה מודל מנטלי ברור, נבחר שיטה מתאימה, ונראה איך מיישמים ב-FastAPI בצורה סטנדרטית.
הגדרות קצרות:
Authentication = מי אתה? (זיהוי משתמש)
Authorization = מה מותר לך? (הרשאות לפעולות/משאבים)
1) מודל מנטלי: בקשה מאובטחת ב-HTTP
ב-HTTP אין “משתמש מחובר” באופן מובנה. כל בקשה עומדת בפני עצמה. לכן אנחנו מוסיפים מזהה זהות לכל בקשה באחת מהצורות הבאות:
- Header: למשל Authorization: Bearer <token>
- Cookie: session cookie או cookie עם token
- Query param: לרוב לא מומלץ (דליף ללוגים/URL)
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) עקרונות אבטחה שלא מדלגים עליהם
- לעולם לא שומרים סיסמה כפי שהיא: משתמשים ב-hash עם salt (bcrypt/argon2).
- HTTPS תמיד: tokens עוברים רק מוצפן.
- Least privilege: נותנים למשתמש רק מה שהוא צריך.
- Rate limiting: מגנים מפני brute-force וניצול API.
- Logging זהיר: לא לוגים של tokens או סיסמאות.
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 (קצרה ומעשית)
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 (בענן)
כששירות אחד מדבר עם שירות אחר, לרוב לא משתמשים בסיסמאות משתמש. פתרונות תעשייתיים:
- API keys עם רוטציה
- JWT חתום בין שירותים (claims של service identity)
- Cloud IAM (למשל Cloud Run Invoker + service account)
7) תרגול
שאלות בדיקה (10)
- מה ההבדל בין Authentication ל-Authorization?
- למה JWT מתאים למערכת stateless כמו Cloud Run?
- למה לא שומרים סיסמאות כפי שהן?
- מה הסיכון בלשים token ב-query param?
- מה היתרון ב-Access token קצר חיים?
- מה זה refresh token rotation ולמה הוא חשוב?
- מה ההבדל בין API key ל-JWT מבחינת “זהות”?
- מה ההבדל בין 401 ל-403?
- מתי תעדיף OAuth2/OIDC על JWT פנימי?
- איך 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).