FA-TOOLS — Header Component
کدهای آماده authentication با FastAPI

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

کدهای آماده authentication با FastAPI — تصویر 1

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

کدهای آماده authentication با FastAPI — تصویر 2

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

کدهای آماده authentication با FastAPI — تصویر 3

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

کدهای آماده authentication با FastAPI

امنیت، ستون فقرات هر اپلیکیشن وب مدرن است و بدون آن، اعتماد کاربران و یکپارچگی داده‌ها به خطر می‌افتد. در دنیای API محور امروز، احراز هویت (Authentication) اولین خط دفاعی برای تضمین دسترسی مجاز به منابع است. FastAPI، با سرعت خیره‌کننده و سهولت بی‌نظیر خود، انتخابی عالی برای ساخت API‌های قدرتمند و امن به شمار می‌رود. اما چگونه می‌توانیم فرآیند پیچیده احراز هویت را در این فریم‌ورک سریع به شکلی استاندارد و کارآمد پیاده‌سازی کنیم؟ در این مقاله، به ارائه کدهای آماده و گام به گام برای پیاده‌سازی احراز هویت بر پایه OAuth2 و JSON Web Tokens (JWT) در FastAPI می‌پردازیم تا شما بتوانید با کمترین تلاش، امنیت را در پروژه‌های خود نهادینه کنید.

خلاصه مقاله در یک نگاه:

  • اهمیت Authentication: چرا امنیت و احراز هویت در API ضروری است.
  • مبانی OAuth2 و JWT: درک عملکرد توکن‌ها و جریان احراز هویت.
  • Hashing کلمه عبور: روش‌های امن ذخیره‌سازی رمز عبور.
  • پیش‌نیازها: نصب کتابخانه‌ها و راه‌اندازی اولیه پروژه.
  • کدهای آماده گام به گام: مدل‌سازی، مدیریت کاربر، تولید/اعتبارسنجی JWT و روت‌های API.
  • بهترین روش‌ها: نکات امنیتی و عملکردی برای یک سیستم قوی.
  • عیب‌یابی سریع: حل مشکلات رایج در پیاده‌سازی.
  • پرسش‌های متداول: پاسخ به سوالات کلیدی کاربران.

با این راهنمای جامع، API خود را با FastAPI به طور امن و کارآمد احراز هویت کنید.

در ادامه، برای سهولت دسترسی به بخش‌های مختلف مقاله، فهرست مطالب را مشاهده می‌کنید:

چرا Authentication در FastAPI حیاتی است؟

تصور کنید یک ساختمان دارید که درب ورودی آن قفل ندارد. هر کسی می‌تواند وارد شود و به هر اتاقی که می‌خواهد سر بزند. در دنیای نرم‌افزار، این سناریو یعنی فاجعه. Authentication یا احراز هویت، دقیقاً نقش همان قفل و کلید را بازی می‌کند. با احراز هویت، شما اطمینان حاصل می‌کنید که فقط کاربران مجاز به منابع خاصی در API شما دسترسی دارند.

  • حفاظت از داده‌ها: اطلاعات حساس کاربران، مانند جزئیات شخصی، مالی یا محتوای خصوصی، باید در برابر دسترسی غیرمجاز محافظت شوند.
  • کنترل دسترسی: تعیین اینکه کدام کاربر به کدام قسمت از API شما دسترسی دارد (برای مثال، یک کاربر عادی نمی‌تواند اطلاعات یک کاربر ادمین را تغییر دهد).
  • یکپارچگی سیستم: جلوگیری از ورود داده‌های مخرب یا تغییرات ناخواسته در سیستم توسط افراد سودجو.
  • پاسخگویی: ثبت فعالیت‌های کاربران مجاز برای ردیابی و عیب‌یابی در صورت لزوم.

FastAPI با ارائه ابزارهای قدرتمند و قابلیت‌های تزریق وابستگی (Dependency Injection)، پیاده‌سازی مکانیزم‌های امنیتی را به شکل قابل توجهی ساده‌تر و سازمان‌یافته‌تر کرده است.

مبانی Authentication در FastAPI

قبل از اینکه به کدها شیرجه بزنیم، باید با مفاهیم اصلی که اساس کار احراز هویت ما در FastAPI را تشکیل می‌دهند، آشنا شویم.

OAuth2 و JWT: قلب تپنده امنیت

FastAPI به طور پیش‌فرض از استاندارد OAuth2 برای امنیت API پشتیبانی می‌کند. در اینجا، ما از گرانتیپ “Password Flow” استفاده می‌کنیم که برای APIهای داخلی یا فرانت‌اندهایی که به API اعتماد دارند، بسیار رایج است. در این سناریو، کاربر نام کاربری و رمز عبور خود را به API ارسال می‌کند و در ازای آن، یک توکن دریافت می‌کند.

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

JSON Web Tokens (JWT):

JWTها توکن‌های امن و فشرده‌ای هستند که اطلاعات مربوط به هویت کاربر را در خود جای می‌دهند. این توکن‌ها از سه بخش تشکیل شده‌اند که با نقطه (.) از هم جدا می‌شوند:

  • Header: شامل نوع توکن (JWT) و الگوریتم هشینگ (مثلاً HS256).
  • Payload: حاوی “ادعاها” (claims) یا اطلاعاتی درباره کاربر (مانند شناسه کاربر، نقش، و زمان انقضا).
  • Signature: برای اطمینان از اینکه توکن در طول مسیر دستکاری نشده است، با استفاده از یک Secret Key و Header و Payload هش می‌شود.

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

Hashing کلمه عبور: دفاعی مستحکم

هیچ‌گاه نباید کلمه عبور کاربران را به صورت متنی (Plain Text) در پایگاه داده ذخیره کنید. این یک اشتباه امنیتی فاحش است که می‌تواند منجر به افشای اطلاعات حساس شود. به جای آن، باید کلمه عبور را “هش” (Hash) کنید.

  • هشینگ چیست؟ فرآیندی یک‌طرفه است که یک رشته ورودی (کلمه عبور) را به یک رشته خروجی با طول ثابت (هش) تبدیل می‌کند. مهم است که از یک الگوریتم هشینگ قوی و کند مانند bcrypt استفاده شود تا حملات Brute-force دشوار شوند.
  • Salt چیست؟ یک رشته تصادفی و منحصربه‌فرد است که قبل از هشینگ به کلمه عبور اضافه می‌شود. Salt تضمین می‌کند که حتی اگر دو کاربر کلمه عبور یکسانی داشته باشند، هش‌های متفاوتی خواهند داشت.

در این مقاله، از کتابخانه `passlib` با الگوریتم `bcrypt` برای هشینگ کلمه عبور استفاده خواهیم کرد.

پیش‌نیازها و راه‌اندازی اولیه

برای شروع، مطمئن شوید که Python 3.7+ روی سیستم شما نصب شده است. سپس، یک محیط مجازی ایجاد کرده و پکیج‌های مورد نیاز را نصب کنید:

python -m venv venv
source venv/bin/activate  # در لینوکس/مک
venvScriptsactivate     # در ویندوز

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

توضیح پکیج‌ها:

  • fastapi: فریم‌ورک اصلی.
  • uvicorn: سرور ASGI برای اجرای FastAPI.
  • python-multipart: برای پارس کردن فرم‌های داده‌ای، که توسط OAuth2PasswordRequestForm استفاده می‌شود.
  • passlib[bcrypt]: برای هشینگ و اعتبارسنجی کلمه عبور.
  • python-jose[cryptography]: برای کار با JWT (ساخت، امضا، اعتبارسنجی).

پیاده‌سازی گام به گام: کدهای آماده Authentication

حالا به بخش اصلی مقاله می‌رسیم: کدهای آماده و توضیح جامع آن‌ها. ساختار پروژه ما می‌تواند ساده باشد؛ تمام کدها را فعلاً در یک فایل main.py قرار می‌دهیم، اما در پروژه‌های بزرگتر حتماً ماژولار کنید.

# main.py
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr

# ----------------------------------------------------
# 1. تنظیمات اولیه و متغیرهای محیطی
# ----------------------------------------------------
# این کلید باید در محیط واقعی از طریق متغیرهای محیطی (Environment Variables) دریافت شود
# و نباید در کد سورس قرار گیرد!
SECRET_KEY = "your-super-secret-key" # این را حتماً تغییر دهید!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # اعتبار توکن در دقیقه

# ----------------------------------------------------
# 2. مدل‌های داده (Pydantic Models)
# ----------------------------------------------------
# مدل برای کاربر در پایگاه داده (یا هر سیستم ذخیره‌سازی)
class UserInDB(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    hashed_password: str
    disabled: Optional[bool] = False

# مدل برای دریافت اطلاعات کاربر جدید هنگام ثبت‌نام
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# مدل برای نمایش اطلاعات کاربر به کلاینت (بدون هش کلمه عبور)
class User(BaseModel):
    username: str
    email: Optional[EmailStr] = None
    disabled: Optional[bool] = False

# مدل برای توکن JWT که بعد از لاگین برگردانده می‌شود
class Token(BaseModel):
    access_token: str
    token_type: str

# مدل برای داده‌های داخل توکن (Payload)
class TokenData(BaseModel):
    username: Optional[str] = None

# ----------------------------------------------------
# 3. مدیریت کلمه عبور و هشینگ
# ----------------------------------------------------
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """کلمه عبور ساده را با هش ذخیره شده مقایسه می‌کند."""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """کلمه عبور ساده را هش می‌کند."""
    return pwd_context.hash(password)

# ----------------------------------------------------
# 4. دیتابیس کاربران (مثال ساده در حافظه)
# در پروژه واقعی، این بخش با دیتابیس واقعی (SQLAlchemy, MongoDB, etc.) جایگزین می‌شود.
# ----------------------------------------------------
fake_users_db = {
    "john.doe": {
        "username": "john.doe",
        "email": "john@example.com",
        "hashed_password": get_password_hash("securepassword"),
        "disabled": False,
    }
}

def get_user_from_db(username: str) -> Optional[UserInDB]:
    """کاربر را از دیتابیس (حافظه) بازیابی می‌کند."""
    user_dict = fake_users_db.get(username)
    if user_dict:
        return UserInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
    """کاربر را بر اساس نام کاربری و کلمه عبور احراز هویت می‌کند."""
    user = get_user_from_db(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

# ----------------------------------------------------
# 5. توابع JWT (ساخت، رمزگشایی و اعتبارسنجی)
# ----------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """یک توکن دسترسی JWT ایجاد می‌کند."""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> Optional[TokenData]:
    """توکن JWT را رمزگشایی و اعتبارسنجی می‌کند."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            return None
        token_data = TokenData(username=username)
    except JWTError:
        return None
    return token_data

# ----------------------------------------------------
# 6. وابستگی‌های امنیتی (Security Dependencies)
# ----------------------------------------------------
# این شیء FastAPI را قادر می‌سازد تا توکن JWT را از هدر "Authorization" بخواند.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """کاربر فعلی را بر اساس توکن JWT بازیابی می‌کند."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    token_data = decode_access_token(token)
    if token_data is None:
        raise credentials_exception
    user = get_user_from_db(token_data.username)
    if user is None:
        raise credentials_exception
    return User(**user.dict())

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """کاربر فعال فعلی را بازیابی می‌کند."""
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user

# ----------------------------------------------------
# 7. تعریف اپلیکیشن FastAPI و روت‌ها
# ----------------------------------------------------
app = FastAPI(
    title="FastAPI Authentication Boilerplate",
    description="A ready-to-use authentication system using FastAPI, OAuth2, and JWT.",
    version="1.0.0",
)

@app.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register_user(user_data: UserCreate):
    """ثبت‌نام کاربر جدید."""
    if get_user_from_db(user_data.username):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already registered"
        )
    
    hashed_password = get_password_hash(user_data.password)
    new_user = UserInDB(
        username=user_data.username,
        email=user_data.email,
        hashed_password=hashed_password,
        disabled=False
    )
    fake_users_db[new_user.username] = new_user.dict()
    return User(**new_user.dict())


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """دریافت توکن دسترسی برای احراز هویت."""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """بازیابی اطلاعات کاربر فعلی (روت محافظت شده)."""
    return current_user

@app.get("/items/")
async def read_items(current_user: User = Depends(get_current_active_user)):
    """یک روت محافظت شده دیگر برای نمایش کالاها."""
    return [{"item_id": "Foo", "owner": current_user.username}, {"item_id": "Bar", "owner": current_user.username}]

# ----------------------------------------------------
# برای اجرای اپلیکیشن: uvicorn main:app --reload
# ----------------------------------------------------

مدل‌های داده (Pydantic Models)

Pydantic به ما کمک می‌کند تا ساختار داده‌های ورودی و خروجی را به وضوح تعریف کنیم، که هم به اعتبارسنجی داده‌ها کمک می‌کند و هم مستندسازی خودکار API را بهبود می‌بخشد. مدل‌های بالا برای کاربر، توکن و اطلاعات درون توکن تعریف شده‌اند.

مدیریت کاربران (User Management)

در کد نمونه، ما از یک دیکشنری ساده (fake_users_db) برای شبیه‌سازی پایگاه داده استفاده کردیم. در یک سناریوی واقعی، شما این بخش را با ORM دلخواه خود (مثل SQLAlchemy با PostgreSQL، MongoDB یا هر دیتابیس دیگری) جایگزین می‌کنید. توابع get_user_from_db و authenticate_user مسئول تعامل با “پایگاه داده” برای پیدا کردن و اعتبارسنجی کاربران هستند.

تولید و اعتبارسنجی JWT

تابع create_access_token داده‌ها (معمولاً نام کاربری) و زمان انقضا را گرفته و یک JWT امضا شده برمی‌گرداند. این توکن برای مدت زمان مشخصی معتبر خواهد بود. تابع decode_access_token نیز وظیفه رمزگشایی و اعتبارسنجی امضای توکن را بر عهده دارد تا از دستکاری شدن آن جلوگیری کند.

وابستگی‌های امنیتی (Security Dependencies)

OAuth2PasswordBearer یک ابزار قدرتمند FastAPI است که به صورت خودکار توکن را از هدر Authorization: Bearer استخراج می‌کند. توابع get_current_user و get_current_active_user از این شیء و تابع decode_access_token استفاده می‌کنند تا کاربر فعلی را شناسایی و اعتبارسنجی کنند. این توابع به عنوان Depends در روت‌های محافظت شده استفاده می‌شوند.

روت‌های Authentication (Login, Register, Get User)

  • /register (POST): به کاربران اجازه می‌دهد تا با نام کاربری، ایمیل و کلمه عبور جدید ثبت‌نام کنند. کلمه عبور قبل از ذخیره هش می‌شود.
  • /token (POST): کاربر با ارسال نام کاربری و کلمه عبور به این روت، یک توکن دسترسی (access token) دریافت می‌کند.
  • /users/me (GET): این یک روت محافظت شده است. برای دسترسی به آن، کاربر باید یک توکن معتبر در هدر Authorization ارسال کند. FastAPI از طریق get_current_active_user، کاربر را شناسایی کرده و اطلاعات آن را برمی‌گرداند.
  • /items/ (GET): یک نمونه دیگر از روت محافظت شده که برای نمایش نحوه استفاده از احراز هویت در سایر بخش‌های API کاربرد دارد.

جدول آموزشی: مقایسه روش‌های ذخیره‌سازی JWT در سمت کلاینت

روش ذخیره‌سازی مزایا و ملاحظات امنیتی
HTTP-Only Cookies
  • مزایا: مقاوم در برابر حملات XSS (Cross-Site Scripting)، به سادگی توسط جاوااسکریپت قابل دسترسی نیست.
  • معایب: آسیب‌پذیر در برابر حملات CSRF (Cross-Site Request Forgery) (نیاز به پیاده‌سازی مکانیزم‌های ضد-CSRF). مدیریت پیچیده‌تر در SPAها و اپلیکیشن‌های موبایل.
Local Storage / Session Storage
  • مزایا: استفاده آسان با جاوااسکریپت، مناسب برای SPAها.
  • معایب: آسیب‌پذیر در برابر حملات XSS. اگر کد مخرب بتواند در صفحه تزریق شود، به سادگی به توکن دسترسی پیدا می‌کند. توصیه نمی‌شود.
Memory (RAM)
  • مزایا: امن‌ترین گزینه از نظر XSS و CSRF، توکن فقط تا زمانی که تب مرورگر باز است در حافظه می‌ماند.
  • معایب: با رفرش صفحه یا بستن تب، توکن از بین می‌رود و کاربر نیاز به لاگین مجدد دارد. (نیاز به Refresh Token).

نکته: برای اکثر موارد، ترکیب HTTP-Only Cookies برای Access Token (کوتاه مدت) و Refresh Token (بلند مدت) با مکانیزم‌های ضد-CSRF پیشنهاد می‌شود.

بهترین روش‌ها و ملاحظات امنیتی

پیاده‌سازی صرف احراز هویت کافی نیست؛ باید آن را به بهترین شکل ممکن ایمن‌سازی کنید:

  • SECRET_KEY را امن نگه دارید: هرگز SECRET_KEY را به صورت مستقیم در کد سورس قرار ندهید. همیشه آن را از طریق متغیرهای محیطی (Environment Variables) دریافت کنید. برای تولید یک کلید قوی و رندوم، می‌توانید از openssl rand -hex 32 یا پکیج secrets در پایتون استفاده کنید.
  • زمان انقضای توکن (Token Expiry): توکن‌های دسترسی (Access Token) باید زمان انقضای کوتاهی داشته باشند (مثلاً ۱۵ تا ۳۰ دقیقه). این کار ریسک سوءاستفاده در صورت به سرقت رفتن توکن را کاهش می‌دهد.
  • Refresh Tokens: برای بهبود تجربه کاربری بدون به خطر انداختن امنیت، می‌توانید از Refresh Tokenها استفاده کنید. Refresh Tokenها طول عمر بیشتری دارند و در یک کوکی HTTP-Only و امن ذخیره می‌شوند. آنها برای درخواست Access Token جدید استفاده می‌شوند و نیاز به لاگین مجدد کاربر را کاهش می‌دهند.
  • Rate Limiting: برای جلوگیری از حملات Brute-force روی نقاط پایانی (Endpoints) مانند /token و /register، نرخ درخواست‌ها را محدود کنید.
  • HTTPS: همیشه از HTTPS برای تمامی ارتباطات API خود استفاده کنید. این کار تضمین می‌کند که توکن‌ها و اطلاعات احراز هویت در حین انتقال رمزگذاری می‌شوند.
  • Error Handling مناسب: پیام‌های خطا باید عمومی باشند و جزئیات زیادی در مورد دلیل شکست احراز هویت فاش نکنند (مثلاً به جای “نام کاربری یافت نشد”، از “نام کاربری یا رمز عبور اشتباه است” استفاده کنید).
  • Logging: فعالیت‌های مربوط به احراز هویت (مانند تلاش‌های ناموفق لاگین) را ثبت کنید تا بتوانید الگوهای مشکوک را شناسایی کنید.

عیب‌یابی سریع (Troubleshooting)

ممکن است در حین پیاده‌سازی با مشکلاتی روبرو شوید. در اینجا به چند مورد رایج و راه‌حل آن‌ها اشاره می‌کنیم:

  • خطای 401 Unauthorized یا "Could not validate credentials":

    دلایل احتمالی:

    • توکن JWT در هدر Authorization: Bearer به درستی ارسال نشده است.
    • SECRET_KEY در زمان ساخت و اعتبارسنجی توکن یکسان نیست.
    • توکن منقضی شده است.
    • توکن دستکاری شده و امضای آن نامعتبر است.
    • کاربر غیرفعال (disabled) شده است.

    راه‌حل:

    • مطمئن شوید که توکن به درستی در هدر Authorization با پیشوند Bearer ارسال می‌شود.
    • SECRET_KEY را در main.py و هر جای دیگری که توکن را امضا یا اعتبارسنجی می‌کنید، بررسی کنید تا یکسان باشند.
    • زمان انقضای توکن را در زمان ساخت چک کنید.

  • خطای "Incorrect username or password" هنگام لاگین:

    دلایل احتمالی:

    • نام کاربری یا کلمه عبور اشتباه است.
    • مشکل در تابع authenticate_user یا verify_password.

    راه‌حل:

    • اعتبارات ورودی را دوباره چک کنید.
    • با چاپ کردن (print) مقادیر در توابع authenticate_user و verify_password، ببینید آیا مقادیر صحیح به آنها می‌رسد یا نه.
    • مطمئن شوید get_password_hash و verify_password از یک CryptContext با یک scheme یکسان استفاده می‌کنند.

  • خطای "jwt.exceptions.JWTError: Signature verification failed":

    دلایل احتمالی:

    • SECRET_KEY متفاوت از کلیدی است که با آن توکن امضا شده.
    • توکن در جایی دستکاری شده است.

    راه‌حل:

    • اطمینان حاصل کنید که SECRET_KEY در سراسر برنامه شما (هنگام ساخت و اعتبارسنجی) یکسان و ثابت است. این یکی از شایع‌ترین غلطات است.

  • مشکلات وابستگی (Dependency Issues) یا ModuleNotFoundError:

    دلایل احتمالی:

    • پکیج‌های لازم به درستی نصب نشده‌اند.
    • محیط مجازی فعال نیست.

    راه‌حل:

    • دستور pip install -r requirements.txt (اگر فایل requirements.txt دارید) یا دستورات نصب در بخش “پیش‌نیازها” را دوباره اجرا کنید.
    • مطمئن شوید محیط مجازی شما فعال است.

پرسش‌های متداول (FAQ)

سوالات پرتکرار در مورد Authentication با FastAPI

JWT چیست و چرا از آن استفاده می‌کنیم؟

JWT (JSON Web Token) یک استاندارد فشرده و امن برای تبادل اطلاعات بین دو طرف به عنوان یک شیء JSON است. از آن استفاده می‌کنیم چون stateless است (سرور نیازی به نگهداری سشن ندارد)، مقیاس‌پذیری بالایی دارد و به راحتی بین سرویس‌ها منتقل می‌شود.

چرا باید کلمه عبور را هش کنیم و چگونه؟

کلمه عبور را هش می‌کنیم تا در صورت نشت اطلاعات پایگاه داده، رمز عبور کاربران فاش نشود. هشینگ فرآیندی یک‌طرفه است. برای هشینگ، از کتابخانه‌هایی مانند passlib با الگوریتم‌های قوی و کند مانند bcrypt استفاده می‌کنیم که به طور خودکار salt را نیز اضافه می‌کنند.

آیا Refresh Token ضروری است؟

ضروری نیست اما برای تجربه کاربری بهتر و امنیت بیشتر توصیه می‌شود. Access Token‌ها کوتاه مدت هستند و Refresh Token‌ها با طول عمر بیشتر، به کاربران اجازه می‌دهند بدون نیاز به لاگین مجدد، Access Token جدیدی دریافت کنند. این کار ریسک امنیتی Access Token را کاهش می‌دهد.

چگونه Secret Key را امن نگه داریم؟

SECRET_KEY باید یک کلید قوی، رندوم و محرمانه باشد که هرگز در کد سورس کامیت (commit) نشود. بهترین روش این است که آن را از طریق متغیرهای محیطی (Environment Variables) در زمان استقرار (deployment) برنامه به آن تزریق کنید. هرگز آن را به صورت دستی در فایل .env بدون پوشش امنیتی نگهداری نکنید، همیشه .env را به .gitignore اضافه کنید.

چه زمانی باید از OAuth2 استفاده کنیم؟

OAuth2 یک استاندارد برای تفویض اختیار (authorization) است، نه فقط احراز هویت. در FastAPI، از بخش OAuth2 “Password Flow” برای احراز هویت استفاده می‌شود که مناسب اپلیکیشن‌های فرانت‌اِند (مانند SPAها یا اپ‌های موبایل) است که به API شما اعتماد دارند و نیاز به توکن برای دسترسی به منابع محافظت شده دارند.

امیدواریم این مقاله جامع و کدهای آماده ارائه شده، راهنمای کاملی برای شما در پیاده‌سازی سیستم احراز هویت قدرتمند و امن در FastAPI باشد. با استفاده از این الگوها و رعایت بهترین روش‌های امنیتی، می‌توانید API‌هایی بسازید که هم سریع باشند و هم قابل اعتماد. فراموش نکنید که امنیت یک فرآیند مداوم است و همیشه باید به‌روزرسانی‌ها و تهدیدهای جدید را مد نظر قرار دهید. کدنویسی امن و موفق باشید!

Table of Contents

آخرین نوشته‌ها