وقتی کاربر نام کاربری و رمز عبورش رو برای ورود ارسال میکنه، سرور فقط همون لحظه میتونه مطمئن بشه که اطلاعات ورود درسته. اما کاربر بعد از Login قراره ده‌ها درخواست دیگه برای دیدن پروفایل، ثبت سفارش یا دسترسی به بخش‌های خصوصی API بفرسته و منطقی نیست همراه هر Request دوباره Password خودش رو ارسال کنه. اینجاست که مفهوم توکن دسترسی وارد ماجرا میشه؛ سرور بعد از تأیید اطلاعات ورود یه Token به کاربر میده و درخواست‌های بعدی با همون Token شناسایی میشن.

یکی از روش‌های رایج برای این کار استفاده از JWT یا JSON Web Token هست. JWT یه رشته امضاشده‌ست که میتونه اطلاعات محدودی مثل شناسه کاربر و زمان انقضا رو داخل خودش داشته باشه. نکته مهم اینه که JWT به صورت پیش‌فرض رمزنگاری‌شده نیست؛ محتوای اون قابل خواندنه، اما امضای دیجیتال اجازه میده سرور بفهمه Token دستکاری شده یا نه. مستندات رسمی FastAPI هم در نمونه امنیتی خودش از JWT همراه با OAuth2، PyJWT و Hash امن Password استفاده میکنه.

Authentication در FastAPI یعنی چه؟

قبل از رفتن سراغ JWT بهتره دو مفهوم رو از هم جدا کنیم. احراز هویت یا Authentication یعنی بفهمیم کاربری که Request فرستاده واقعاً کیه؛ مثلاً بررسی کنیم Username و Password متعلق به یه حساب معتبر هست یا نه. بعد از اینکه هویت کاربر مشخص شد، مرحله بعد میتونه بررسی سطح دسترسی اون باشه؛ مثلاً آیا این کاربر اجازه حذف محصول یا ورود به پنل مدیریت رو داره؟

JWT بیشتر در بخش اول کمک میکنه. بعد از Login موفق، سرور اطلاعات محدودی درباره کاربر رو داخل Token قرار میده، Token رو امضا میکنه و به Client برمیگردونه. از اون لحظه Client میتونه به جای ارسال دوباره Password، Token رو همراه Requestهای بعدی بفرسته.

جریان JWT Authentication چطور کار می‌کند؟

جریان ساده رو میشه اینطوری دید: کاربر Username و Password خودش رو به Endpoint ورود میفرسته، سرور اطلاعات رو بررسی میکنه و اگه درست باشن یه Access Token میسازه. بعد Client اون Token رو ذخیره میکنه و برای دسترسی به Endpointهای محافظت‌شده همراه درخواست میفرسته.

در استاندارد Bearer Token معمولاً Token داخل Header مربوط به مجوز دسترسی ارسال میشه. FastAPI ابزار OAuth2PasswordBearer رو در اختیارمون میذاره تا دریافت این Token و اتصالش به سیستم Dependency راحت‌تر بشه. مستندات رسمی FastAPI هم توضیح میدن که Bearer Token از طریق Header مربوط به Authorization دریافت میشه و ابزارهای امنیتی FastAPI به صورت خودکار با مستندات OpenAPI و صفحه /docs هماهنگ میشن.

Access Token چیست؟

Access Token همون چیزی هست که Client بعد از Login دریافت میکنه و برای Requestهای بعدی استفاده میکنه. این Token نباید تا ابد معتبر بمونه، چون اگه به هر دلیلی در اختیار فرد دیگه‌ای قرار بگیره، تا زمانی که معتبره میتونه برای دسترسی به API استفاده بشه.

به همین دلیل معمولاً Access Token دارای زمان انقضاست. برای مثال میشه Token رو طوری ساخت که بعد از ۳۰ دقیقه منقضی بشه. مستندات فعلی FastAPI هم در نمونه JWT خودش یه زمان انقضا برای Access Token تعریف میکنه و هنگام Decode کردن Token، اعتبار امضا و زمان انقضای اون رو بررسی میکنه.

ساخت JWT در FastAPI

برای ساخت و بررسی JWT میشه از کتابخونه PyJWT استفاده کرد. همچنین Password کاربران نباید به صورت خام داخل دیتابیس ذخیره بشه و باید قبل از ذخیره شدن Hash بشه. در مستندات رسمی فعلی FastAPI، برای Hash و بررسی Password از pwdlib استفاده شده و تنظیمات پیشنهادی اون میتونه از الگوریتم‌های مناسب Hash کردن رمز استفاده کنه.

نمونه زیر جریان اصلی رو در یه مثال ساده نشون میده:

from datetime import datetime, timedelta, timezone

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from pwdlib import PasswordHash

app = FastAPI()

SECRET_KEY = "replace-with-a-secure-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

password_hash = PasswordHash.recommended()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

fake_user = {
    "username": "amir",
    "hashed_password": password_hash.hash("123456")
}


def create_access_token(username: str):
    expire = datetime.now(timezone.utc) + timedelta(
        minutes=ACCESS_TOKEN_EXPIRE_MINUTES
    )

    payload = {
        "sub": username,
        "exp": expire
    }

    return jwt.encode(
        payload,
        SECRET_KEY,
        algorithm=ALGORITHM
    )


def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM]
        )

        username = payload.get("sub")

        if username != fake_user["username"]:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token"
            )

        return username

    except InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token"
        )


@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    if form_data.username != fake_user["username"]:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password"
        )

    if not password_hash.verify(
        form_data.password,
        fake_user["hashed_password"]
    ):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password"
        )

    access_token = create_access_token(form_data.username)

    return {
        "access_token": access_token,
        "token_type": "bearer"
    }


@app.get("/profile")
async def profile(username: str = Depends(get_current_user)):
    return {
        "username": username,
        "message": "Private profile data"
    }

در این مثال کاربر ابتدا اطلاعات ورود خودش رو برای /token ارسال میکنه. اگه Username و Password درست باشن، تابع create_access_token یه Token دارای زمان انقضا تولید میکنه. بعد Endpoint مربوط به /profile مستقیماً Password رو بررسی نمیکنه؛ به جای اون از get_current_user استفاده میکنه تا Token دریافت‌شده رو Decode و اعتبارسنجی کنه.

اطلاعات داخل JWT را محرمانه فرض نکنید

یکی از اشتباه‌های رایج اینه که چون JWT ظاهر عجیب و طولانی داره، فکر کنیم اطلاعات داخل اون رمزنگاری شده. این تصور درست نیست. JWT معمولی که با الگوریتم‌هایی مثل HS256 امضا میشه، اطلاعات Payload رو مخفی نمیکنه و شخصی که Token رو داشته باشه میتونه محتوای اون رو Decode کنه. چیزی که Signature تضمین میکنه اینه که تغییر دادن Token بدون داشتن Secret Key قابل تشخیص باشه.

پس Password، شماره کارت، Secret Key یا اطلاعات محرمانه رو داخل Payload قرار ندید. معمولاً یه شناسه مثل User ID یا Username به همراه اطلاعاتی مثل زمان انقضا برای شناسایی کاربر کافیه.

sub داخل JWT چیست؟

یکی از Claimهای رایج JWT با نام sub شناخته میشه که معنی ساده اون هویت اصلی Token یا Subject هست. در نمونه بالا Username داخل sub ذخیره شده تا وقتی Token دوباره به سرور رسید بفهمیم متعلق به کدوم کاربره.

در پروژه واقعی معمولاً استفاده از یه شناسه ثابت مثل User ID انتخاب مناسب‌تریه، چون Username ممکنه تغییر کنه. بعد از Decode کردن Token میشه اون شناسه رو از دیتابیس پیدا کرد و وضعیت واقعی کاربر رو بررسی کرد.

این نکته مهمه، چون نباید صرفاً به اطلاعات داخل Token اعتماد کنیم و تصور کنیم کاربر هنوز در دیتابیس فعال یا موجوده. ممکنه حساب کاربر بعد از صدور Token غیرفعال یا حذف شده باشه.

محافظت از Endpointها با Depends

یکی از قسمت‌های کاربردی FastAPI اینه که سیستم Security رو میشه با Dependencyها ترکیب کرد. Dependency یعنی بخشی از منطق مشترک که FastAPI قبل از اجرای Endpoint اجراش میکنه.

در مثال بالا، /profile به get_current_user وابسته شده. پس قبل از اینکه کد اصلی Endpoint اجرا بشه، Token بررسی میشه. اگه Token وجود نداشته باشه، نامعتبر باشه یا منقضی شده باشه، Request متوقف میشه و کاربر پاسخ 401 Unauthorized دریافت میکنه.

همین الگو باعث میشه برای ده‌ها Endpoint خصوصی مجبور نباشید منطق Decode کردن JWT رو دوباره بنویسید. کافیه Dependency بررسی کاربر رو روی Endpointهای موردنظر قرار بدید.

Secret Key را داخل سورس‌کد واقعی نگذارید

در مثال آموزشی Secret Key داخل فایل قرار گرفته تا جریان کار مشخص باشه، اما در پروژه واقعی این مقدار نباید داخل Repository نوشته بشه. Secret Key در واقع کلیدی هست که سرور با اون Token رو امضا میکنه و اگه شخص دیگه‌ای به اون دسترسی پیدا کنه، میتونه Tokenهایی تولید کنه که از نظر سرور معتبر به نظر برسن.

بهتره Secret Key از Environment Variable یا سیستم مدیریت Secret دریافت بشه. حتی مستندات FastAPI برای تولید یه Secret تصادفی امن، استفاده از یه کلید تصادفی قوی رو توصیه میکنن و تأکید دارن که مقدار نمونه نباید در پروژه واقعی استفاده بشه.

JWT جای HTTPS را نمی‌گیرد

وجود JWT به این معنی نیست که ارتباط Client و Server امن شده. Token همچنان همراه Request از شبکه عبور میکنه و اگه ارتباط بدون HTTPS باشه امکان لو رفتن اطلاعات وجود داره. OAuth2 هم خودش مسئول رمزنگاری ارتباط نیست و انتظار داره برنامه روی HTTPS ارائه بشه.

بنابراین در محیط Production باید JWT رو همراه HTTPS استفاده کنید، نه به جای اون.

آیا JWT برای Logout دردسر دارد؟

یکی از محدودیت‌های JWT اینه که Token بعد از صدور میتونه تا زمان انقضای خودش معتبر باقی بمونه. اگه فقط Token رو از مرورگر حذف کنید، از دید Client Logout انجام شده، اما نسخه‌ای از همون Token که قبلاً کپی شده ممکنه همچنان قابل استفاده باشه.

برای پروژه‌های ساده معمولاً Access Token کوتاه‌مدت این مشکل رو محدود میکنه. در سیستم‌های حرفه‌ای‌تر ممکنه Refresh Token، لیست Tokenهای مسدودشده یا مکانیزم‌های ابطال Session اضافه بشن. پس JWT ابزار قدرتمندیه، ولی خودش به تنهایی تمام مسائل مربوط به Login، Logout و مدیریت Session رو حل نمیکنه.

اشتباه رایج؛ قرار دادن Permission کامل داخل Token

ممکنه وسوسه بشید نقش کاربر یا سطح دسترسی اون رو داخل JWT قرار بدید و دیگه هرگز دیتابیس رو بررسی نکنید. این روش در بعضی معماری‌ها استفاده میشه، اما باید با دقت انجام بشه؛ چون اگه دسترسی کاربر بعد از صدور Token تغییر کنه، Token قبلی ممکنه تا زمان انقضا همچنان Permission قدیمی رو داشته باشه.

برای پروژه‌های حساس بهتره اطلاعاتی که ممکنه سریع تغییر کنن در زمان Request دوباره از منبع معتبر بررسی بشن یا Tokenها عمر کوتاهی داشته باشن.

جمع‌بندی

JWT Authentication در FastAPI یه روش کاربردی برای اینه که کاربر فقط هنگام Login اطلاعات ورودش رو ارسال کنه و بعد از اون با یه Access Token به Endpointهای خصوصی دسترسی داشته باشه. سرور Token رو امضا میکنه، Client اون رو همراه Requestهای بعدی میفرسته و FastAPI میتونه با ابزارهایی مثل OAuth2PasswordBearer و Depends فرآیند بررسی Token رو به شکل مرتب بین Endpointها به اشتراک بذاره.

در کنار این ساختار باید چند اصل رو جدی گرفت: Passwordها باید Hash بشن، Access Token باید زمان انقضا داشته باشه، Secret Key نباید داخل سورس‌کد واقعی قرار بگیره، اطلاعات محرمانه نباید داخل Payload JWT ذخیره بشن و ارتباط Production باید با HTTPS انجام بشه.

به زبان ساده، Password برای اثبات هویت در زمان ورود استفاده میشه و Access Token برای اثبات همون هویت در Requestهای بعدی؛ هرکدوم جای خودش رو داره.