1.2 אוטנטקציה הרצאה
למה צריך אותנטיקציה (Authentication) ואותריזציה (Authorization)¶
אותנטיקציה (הזדהות) היא הזיהוי של מי שמבקש לגשת למערכת – האם זאת באמת אותה זהות שהיא טוענת להיות.
דמיינו שעלינו לממש מערכת שיודעת לזהות משתמשים, אותנטקציה היא פעולה שנועדה לזהות משתמשים (בדרך כלל על פי שם משתמש וסיסמה)
אותריזציה (הרשאה) היא ההרשאה – מה מותר לאותו משתמש לעשות לאחר שהזדהה בהצלחה. לדוגמה, אחרי שמשתמש מסיום התחבר לפרויקט הטיולים שלי, הוא יוכל ליצור טיולים על שמו.
דוגמה קצרה¶
- משתמש נכנס לאפליקציית משימות.
- הוא מזדהה עם מייל וסיסמה (אותנטיקציה).
- לאחר הזדהות, המשתמש יכול לראות רק את המשימות שלו, ולא של אחרים.
לעומת זאת משתמש עם תפקיד "admin" יוכל גם לצפות בכל המשימות של כל המשתמשים (אותריזציה).
JWT – מה זה ואיך זה עובד¶
הJWT = JSON Web Token. זהו טוקן חתום קריפטוגרפית שבדרך כלל משתמשים בו כדי לבצע אוטנטקציה ואותריזציה (תכף אסביר איך).
בדרך כלל, אחרי שאימתנו משתמש מסוים נניח בעמוד התחברות (שם משתמש וסיסמה) (כלומר אחרי שעבר אוטנטקציה), כדי לאפשר לו לעשות בקשות מסוימות לapi שלנו שדורשות אותורזציה (כמו להעלות טיולים חדשים תחת שם משתמש כלשהו) אנחנו נביא למשתמש ״טוקן״ מסוים (מחרוזת מאוד ארוכה) ולכל בקשה שדרושת אותוריזציה כלשהי, המשתמש ישלח את הטוקן הזה.
ככה נוכל לבדוק את הטוקן בצד שרת, ולראות האם זה באמת המשתמש (והטוקן ששלחנו לו), והאם לבצע את הפעולה שביקש (נניח ליצור טיול חדש).

כך נראה טוקן jwt חתום:
- Header: אלגוריתם החתימה (למשל HS256) וסוג הטוקן.
- Payload: מי המשתמש המזוהה, פרטים עליו, וכמה זמן הטוקן פעיל
- Signature: חתימה שהצד שרת שלנו יכול לאמת שהטוקן הזה יוצר על ידנו ולא גוף אחר
דוגמה להתהליך המלא:
1. המשתמש שולח שם משתמש/סיסמה ל־/login.
2. השרת מאמת את הסיסמה,(עושה אוטנטקציה למשתמש) ויוצר טוקן JWT חתום למשתמש עם תוקף (למשל 30 דקות).
3. הלקוח שומר את הטוקן ושולח אותו בכל בקשה בהדר http הבא: Authorization: Bearer <token>.
4. בכל בקשה לשרת, השרת מאמת את החתימה ואת התוקף, ושולף את זהות המשתמש מה־payload. (כלומר השרת יודע מי המשתמש שמנסה לבצע פעולה כלשהי על פי הpayload שנמצא בטוקן)
קיימים עוד דרכים לבצע אוטנטקציה ואותוריזציה למשתמשים (לא רק טוקנים של jwt), אבל למעשה jwt זה הדרך הכי פרקטית ושימושית בדרך כלל. יש לה כמה יתרונות וחסרונות:
יתרונות: Stateless (כלומר, לא צריך לשמור שום מידע במסד הנתונים, רק לבדוק חתימה קריפטוגרפית), קל לשימוש ב־API.
חסרונות: אי אפשר "לבטל" טוקן (או לחסום משתמש כלשהו מלהשתמש בטוקן שיצרנו) עד שהתוקף שהגדרנו לטוקן (כשיצרנו אותו) יפוג. (אין דרך קלה לזה)
כדי להשתמש בjwt בפייתון ניתן להשתמש בספרייה jose.
from datetime import datetime, timedelta
from typing import Optional
from jose import jwt, JWTError
SECRET_KEY = "VERY_SECRET_KEY_CHANGE_ME"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return token
def decode_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
עירבול - hash¶
בהתהליך היצירת משתמש (הregister), נצטרך לשמור את הסיסמה של שהמשתמש הזין (כדי שנוכל לבצע בהמשך login - אוטנטיקציה).
למעשה נוצרה בעיה בעניין, מה אם האקרים יפרצו למסד הנתונים שלנו? הם יוכלו להשיג את הסיסמאות של כל המשתמשים.
כדי לפתור את הבעיה אנחנו משתמשים בעירבול (hash)- ישנן המון סוגים של פונקציות hash שונות, כאשר כולן יודעות לקחת מחרוזת מסוימת (במקרה שלנו זה הסיסמה) ויודעת להפוך את המחרוזת לטקסט שונה ״טקסט מעורבל״ שלא ניתן להחזיר אחורה.
למשל, פונקציית העירבול bcrypt תקח את המחרוזת ״hello" ותמיר אותה למחרוזת: ״9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043״
הייחוד של פונקציות hash שאין שום דרך להחזיר hash לטקסט המקורי.
במסד הנתונים אנחנו נשמור hash של סיסמאות (אף פעם לא נשמור את הסיסמה עצמה בלי hash) כדי שבמקרה והאקרים יפרצו לנו למסד הנתונים, לא תהיה להם גישה לסיסמאות המשתמשים.
ומכאן עולה השאלה, אבל אם שמרנו את הסיסמה אחרי פעולת hash, אז בתהליך האוטנטקציה (הlogin) כיצד נדע להשוות בין הסיסמה שהמשתמש הכניס לסיסמה במסד הנתונים?
פשוט נעשה hash על הסיסמה שהמשתמש הכניס, ונראה אם הhash שהסיסמה שהמשתמש הזין זהה לhash של הסיסמה במסד הנתונים.
כדי לבצע hash בפייתון ניתן להשתמש בספרייה passlib
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str):
return pwd_context.hash(password)
def verify_password(password: str, hashed: str):
return pwd_context.verify(password, hashed)
מימוש האוטנטקציה בfastapi¶
נוסיף route-ים שיודעים לטפל בlogin + register במצב post כאשר הם תלויים בהאם יש חיבור למסד הנתונים (פונקציית depend)
from fastapi.security import OAuth2PasswordRequestForm
@app.post("/register", response_model=schemas.UserRead)
def register_route(user_in: schemas.UserCreate, db: Session = Depends(get_db)):
return controllers.register_user(user_in, db)
@app.post("/login")
def login_route(user: schemas.UserLogin, db: Session = Depends(get_db)):
return controllers.login_user(user, db)
כאשר נגדיר controller-ים שיודעים לבצע login בdb וregister.
# ---------------------- Users ----------------------
def register_user(user_in: schemas.UserCreate, db: Session):
exists = db.query(models.User).filter(models.User.username == user_in.username).first()
if exists:
raise HTTPException(status_code=400, detail="Username already exists")
hashed = hash_password(user_in.password)
user = models.User(username=user_in.username, password_hash=hashed)
db.add(user)
db.commit()
db.refresh(user)
return user
def login_user(user_in: schemas.UserLogin, db: Session):
user = db.query(models.User).filter(models.User.username == user_in.username).first()
if not user or not verify_password(user_in.password, user.password_hash):
raise HTTPException(status_code=401, detail="Invalid username or password")
token = create_access_token({"sub": user.username, "user_id": user.id})
return {"access_token": token, "token_type": "bearer"}
אחרי שהגדרנו את הlogin והregister, לכל הroute-ים המוגנים, המשתמש יוסיף את הjwt token לheader בבקשה- עלינו לפתח פונקציית depend לכל שיודעת לפרק את הjwt token מהבקשה ולבדוק האם המשתמש עם token תקין.
למעשה, כדי לפתח את הפונקציה הזו, אנחנו יכולים להשתמש בפונקציה מובנית OAuth2PasswordBearer של fastapi שיודעת לפרק את הtoken מבקשות.
from fastapi.security import OAuth2PasswordBearer
from .security import hash_password, verify_password, create_access_token, decode_token
from fastapi import Depends
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(lambda: None)):
payload = decode_token(token)
if not payload:
raise HTTPException(status_code=401, detail="Invalid or expired token")
username = payload.get("sub")
user = db.query(models.User).filter(models.User.username == username).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
עכשיו נוכל פשוט להוסיף את get_current_user כdepend לכל route שנרצה.
למשל:
@app.post("/hotels", response_model=schemas.HotelRead, status_code=status.HTTP_201_CREATED)
def create_hotel_route(
hotel_in: schemas.HotelCreate,
db: Session = Depends(get_db),
current_user = Depends(controllers.get_current_user)
):
return controllers.create_hotel(hotel_in, db)
רק למי שיש jwt token וולידי, וחיבור למסד הנתונים, מלון יווצר.