W świecie nowoczesnych aplikacji rozproszonych JWT (JSON Web Token) stał się standardem autoryzacji, zapewniając bezstanowe i skalowalne uwierzytelnianie użytkowników. W połączeniu z OAuth2, używanym przez platformy takie jak Google, GitHub czy Facebook, pozwala tworzyć bezpieczne i nowoczesne systemy logowania oraz autoryzacji. W tym przewodniku poznasz, jak implementować autoryzację JWT i OAuth2 w Pythonie, jak zapewnić bezpieczeństwo tokenów i jakie są najlepsze praktyki.
Czym jest JWT (JSON Web Token)
JWT to zaszyfrowany token, który zawiera informacje o użytkowniku i jego uprawnieniach. Zamiast trzymać sesję po stronie serwera, JWT pozwala przekazać te dane w postaci podpisanego ciągu tekstowego.
Struktura JWT
JWT składa się z trzech części oddzielonych kropkami:
Bash1xxxxx.yyyyy.zzzzz
- Header – informacje o algorytmie i typie tokena (np.
{"alg": "HS256", "typ": "JWT"}). - Payload – dane użytkownika (np.
user_id,role,exp,iat). - Signature – podpis HMAC lub RSA, który gwarantuje, że token nie został zmodyfikowany.
Przykładowy payload:
JSON1 2 3 4 5 6 7{ "user_id": 123, "username": "admin", "role": "admin", "exp": 1714000000, "iat": 1713996400 }
Token jest przesyłany w nagłówku HTTP:
Bash1Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJhZG1pbiJ9.signature
Zalety JWT
- Bezstanowość — serwer nie musi przechowywać sesji w pamięci lub bazie danych.
- Skalowalność — łatwe w środowiskach rozproszonych i mikroserwisach.
- Przenośność — token zawiera wszystkie potrzebne informacje.
- Kompatybilność — działa z różnymi platformami i językami.
Wady JWT
- Trudność unieważniania — po wydaniu tokena trudno go unieważnić przed wygaśnięciem.
- Rozmiar — tokeny mogą być większe niż sesje ID.
- Bezpieczeństwo — jeśli token zostanie przejęty, atakujący może go używać do czasu wygaśnięcia.
Jak działa autoryzacja z JWT
Proces autoryzacji z JWT składa się z kilku kroków:
- Użytkownik wysyła dane logowania (email, password) do endpointu
/loginlub/token. - Serwer weryfikuje dane — sprawdza, czy użytkownik istnieje i hasło jest poprawne.
- Serwer generuje token JWT — tworzy token z danymi użytkownika i podpisem.
- Klient zapisuje token — frontend lub aplikacja mobilna przechowuje token (najlepiej w HTTP-only cookie).
- Klient wysyła token w kolejnych zapytaniach — w nagłówku
Authorization: Bearer <token>. - Serwer weryfikuje token — sprawdza podpis i ważność tokena przy każdym żądaniu.
Dzięki temu serwer nie musi trzymać sesji w pamięci, a użytkownik pozostaje zalogowany aż do wygaśnięcia tokena.
Implementacja JWT w Pythonie (FastAPI)
FastAPI posiada wbudowane wsparcie dla OAuth2 i JWT, co czyni go idealnym narzędziem do budowy bezpiecznych API.
Instalacja zależności
Bash1pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart
Podstawowa implementacja
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from datetime import datetime, timedelta from typing import Optional app = FastAPI() # Konfiguracja SECRET_KEY = "your-secret-key-change-in-production" # W produkcji użyj zmiennych środowiskowych ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Kontekst do hashowania haseł pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # OAuth2 scheme oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Funkcja do tworzenia tokena def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt # Funkcja do weryfikacji tokena async def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception return username # Endpoint logowania @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends()): # W rzeczywistej aplikacji sprawdź dane w bazie danych if form_data.username != "admin" or form_data.password != "secret": 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": form_data.username}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} # Chroniony endpoint @app.get("/users/me") async def read_users_me(current_user: str = Depends(get_current_user)): return {"user": current_user}
W tym przykładzie endpoint /token generuje token, a /users/me weryfikuje go przy każdym żądaniu.
Pełna implementacja z bazą danych
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36from sqlalchemy.orm import Session from datetime import datetime, timedelta from jose import jwt from passlib.context import CryptContext 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) def authenticate_user(db: Session, username: str, password: str): user = db.query(User).filter(User.username == username).first() if not user: return False if not verify_password(password, user.hashed_password): return False return user @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, 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, "user_id": user.id}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"}
Odświeżanie tokenów i bezpieczeństwo
Tokeny JWT mają ograniczony czas życia (exp). Aby użytkownik nie musiał logować się ponownie po każdym wygaśnięciu tokena, wprowadza się refresh tokeny.
Strategia Access Token + Refresh Token
Najczęstsze podejście:
- access_token – krótko żyjący (np. 15-30 minut), używany do autoryzacji żądań.
- refresh_token – długowieczny (np. 7-30 dni), używany tylko do odświeżenia access tokena.
Implementacja:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45from datetime import datetime, timedelta import secrets REFRESH_TOKEN_EXPIRE_DAYS = 7 def create_refresh_token(user_id: int) -> str: # Refresh token można przechowywać w bazie danych token = secrets.token_urlsafe(32) # Zapisz token w bazie z datą wygaśnięcia db_refresh_token = RefreshToken( token=token, user_id=user_id, expires_at=datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) ) db.add(db_refresh_token) db.commit() return token @app.post("/refresh") async def refresh_token(refresh_token: str, db: Session = Depends(get_db)): db_token = db.query(RefreshToken).filter( RefreshToken.token == refresh_token, RefreshToken.expires_at > datetime.utcnow() ).first() if not db_token: raise HTTPException(status_code=401, detail="Invalid refresh token") # Wygeneruj nowy access token access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": db_token.user.username, "user_id": db_token.user_id}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} @app.post("/logout") async def logout(refresh_token: str, db: Session = Depends(get_db)): # Unieważnij refresh token db_token = db.query(RefreshToken).filter(RefreshToken.token == refresh_token).first() if db_token: db.delete(db_token) db.commit() return {"message": "Logged out successfully"}
Bezpieczne przechowywanie tokenów
Pamiętaj, by:
- Refresh tokeny przechowywać bezpiecznie po stronie klienta — najlepiej w HTTP-only cookies (chroni przed XSS).
- Nie trzymać tokenów w localStorage — są narażone na ataki XSS.
- Stosować HTTPS — tokeny nie mogą być przesyłane po HTTP.
- Używać Secure cookies — cookies powinny być dostępne tylko przez HTTPS.
- Implementować SameSite attribute — chroni przed atakami CSRF.
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20from fastapi import Response @app.post("/token") async def login(form_data: OAuth2PasswordRequestForm = Depends(), response: Response = None): # ... logika autoryzacji ... access_token = create_access_token(data={"sub": form_data.username}) refresh_token = create_refresh_token(user.id) # Ustaw refresh token w HTTP-only cookie response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=True, # Tylko HTTPS samesite="lax", max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 ) return {"access_token": access_token, "token_type": "bearer"}
OAuth2 – standard autoryzacji
OAuth2 to protokół używany przez największe platformy internetowe. Nie służy bezpośrednio do logowania, lecz do delegowania dostępu do zasobów w imieniu użytkownika.
Jak działa OAuth2?
Przykład: aplikacja A (Twój system) chce uzyskać dostęp do danych użytkownika z konta Google.
- Użytkownik klika „Zaloguj przez Google”.
- Następuje przekierowanie do Google OAuth (wraz z
client_idiredirect_uri). - Użytkownik wyraża zgodę na dostęp.
- Google przekierowuje z powrotem z
authorization_code. - Serwer backendowy wymienia kod na
access_tokenirefresh_token. - Aplikacja używa
access_tokendo pobrania danych użytkownika z Google. - Aplikacja zapisuje dane użytkownika i tworzy własną sesję/JWT.
Implementacja OAuth2 w Pythonie
W Pythonie możesz użyć bibliotek:
- Authlib (
pip install authlib) — nowoczesna biblioteka z dobrym wsparciem dla FastAPI. - Requests-OAuthlib — dla prostszych integracji.
Przykład z Authlib:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38from authlib.integrations.starlette_client import OAuth from starlette.config import Config config = Config('.env') oauth = OAuth(config) oauth.register( name='google', server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', client_kwargs={ 'scope': 'openid email profile' } ) @app.get("/login/google") async def login_google(request: Request): redirect_uri = request.url_for('auth_google') return await oauth.google.authorize_redirect(request, redirect_uri) @app.get("/auth/google") async def auth_google(request: Request, db: Session = Depends(get_db)): token = await oauth.google.authorize_access_token(request) user_info = token.get('userinfo') # Znajdź lub utwórz użytkownika user = db.query(User).filter(User.email == user_info['email']).first() if not user: user = User( email=user_info['email'], username=user_info['name'], provider='google' ) db.add(user) db.commit() # Utwórz własny JWT token access_token = create_access_token(data={"sub": user.username, "user_id": user.id}) return {"access_token": access_token, "token_type": "bearer"}
JWT vs OAuth2 – różnice
Zrozumienie różnicy między JWT a OAuth2 jest kluczowe:
| Cecha | JWT | OAuth2 |
|---|---|---|
| Typ | Token autoryzacyjny | Protokół autoryzacji |
| Sesja | Bezstanowa | Może być stanowa lub bezstanowa |
| Użycie | Autoryzacja użytkowników w Twoim API | Logowanie z zewnętrznych dostawców (Google, GitHub, Facebook) |
| Skalowalność | Wysoka | Zależna od implementacji |
| Bezpieczeństwo | Wymaga HTTPS, poprawnego TTL | Złożony, ale bardzo bezpieczny |
| Złożoność | Prosta | Bardziej złożony |
Często oba podejścia są łączone — OAuth2 generuje JWT używane w dalszej komunikacji. OAuth2 służy do uzyskania zgody użytkownika i dostępu do zewnętrznych zasobów, a JWT do autoryzacji w Twoim własnym API.
Dobre praktyki bezpieczeństwa
Przy implementowaniu autoryzacji warto przestrzegać następujących zasad:
1. Zawsze używaj HTTPS
Tokeny nie mogą być przesyłane po HTTP — są narażone na przechwycenie.
Python1 2 3# W produkcji wymuś HTTPS SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True
2. Ustaw krótki TTL dla access tokenów
Access tokeny powinny wygasać szybko (15-30 minut), aby zminimalizować szkody w przypadku przejęcia.
Python1ACCESS_TOKEN_EXPIRE_MINUTES = 15 # Krótki czas życia
3. Weryfikuj podpis i claimy przy każdym żądaniu
Nie tylko sprawdzaj podpis, ale też iss (issuer), aud (audience) i exp (expiration):
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15def verify_token(token: str): try: payload = jwt.decode( token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_signature": True, "verify_exp": True}, issuer="your-app", # Weryfikuj issuer audience="your-api" # Weryfikuj audience ) return payload except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Token expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token")
4. Nie trzymaj tokenów w localStorage
Używaj HTTP-only cookies — są chronione przed JavaScriptem i atakami XSS.
5. Loguj unieważnienia tokenów
Po wylogowaniu użytkownika unieważnij refresh token w bazie danych.
Python1 2 3 4 5 6 7 8 9 10@app.post("/logout") async def logout(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = payload.get("user_id") # Usuń wszystkie refresh tokeny użytkownika db.query(RefreshToken).filter(RefreshToken.user_id == user_id).delete() db.commit() return {"message": "Logged out"}
6. Nie używaj tego samego klucza do podpisywania i szyfrowania
Używaj osobnych kluczy dla różnych celów.
7. Używaj algorytmów asymetrycznych (RS256) w środowiskach produkcyjnych
Dla większego bezpieczeństwa użyj RS256 zamiast HS256:
Python1 2 3 4 5 6 7 8 9 10 11 12# Generowanie kluczy from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) public_key = private_key.public_key() # Podpisywanie tokena token = jwt.encode(payload, private_key, algorithm="RS256") # Weryfikacja tokena payload = jwt.decode(token, public_key, algorithms=["RS256"])
8. Implementuj rate limiting
Ogranicz liczbę prób logowania, aby chronić przed brute force:
Python1 2 3 4 5 6 7 8 9from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.post("/token") @limiter.limit("5/minute") async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()): # ... logika logowania ...
9. Waliduj dane wejściowe
Zawsze waliduj dane logowania i tokeny przed przetworzeniem.
10. Monitoruj podejrzane aktywności
Loguj nieudane próby logowania, użycie nieprawidłowych tokenów i inne podejrzane aktywności.
Podsumowanie
JWT i OAuth2 to fundament bezpiecznej autoryzacji w nowoczesnych API. Dzięki nim możesz tworzyć skalowalne, bezstanowe systemy uwierzytelniania, które łatwo integrują się z frontendem, mikroserwisami i zewnętrznymi dostawcami.
Najważniejsze zasady:
- JWT do prostych, wewnętrznych API — idealny dla własnych aplikacji i mikroserwisów.
- OAuth2 do integracji z zewnętrznymi usługami — Google, GitHub, Facebook.
- Zawsze stosuj szyfrowanie (HTTPS) — bez tego całe bezpieczeństwo traci sens.
- Odświeżanie tokenów — używaj refresh tokenów dla lepszego UX.
- Walidacja po stronie serwera — zawsze weryfikuj tokeny, nawet jeśli wydają się ważne.
- Bezpieczne przechowywanie — HTTP-only cookies zamiast localStorage.
- Krótkie TTL dla access tokenów — minimalizuj ryzyko w przypadku przejęcia.
Pamiętaj, że bezpieczeństwo to proces ciągły — regularnie aktualizuj zależności, monitoruj logi i reaguj na nowe zagrożenia. Dobrze zaprojektowana autoryzacja to podstawa zaufania użytkowników do Twojej aplikacji.



