Spotkanie 2: Projektowanie API i bezpieczeństwo

✅ Sprawdź się ze spotkania 1


🧩 Część I — Projektowanie API REST

🎯 Cele

Nauczysz się projektować adresy URL i strukturę odpowiedzi API tak, żeby były przewidywalne i łatwe w użyciu — oraz poznasz, jak FastAPI generuje dokumentację OpenAPI automatycznie z Waszego kodu.


1️⃣ Zasoby, nie czynności

Adres URL w REST opisuje rzecz (zasób), nie akcję — akcję wyraża metoda HTTP (spotkanie 1, część II). Dobra konwencja nazewnicza:

Źle Dobrze
/getPoligony GET /poligony
/utworzRezerwacje POST /rezerwacje
/poligon/usun/5 DELETE /poligony/5

Zasady:

  • Rzeczowniki, nie czasowniki w ścieżce.
  • Liczba mnoga dla kolekcji: /poligony, nie /poligon.
  • Małe litery, myślniki zamiast podkreślników w wieloczłonowych nazwach.

2️⃣ Zasoby zagnieżdżone

Kiedy jeden zasób “należy” logicznie do drugiego:

GET /poligony/5/rezerwacje        -- rezerwacje na poligonie 5
POST /poligony/5/rezerwacje       -- nowa rezerwacja na poligonie 5
GET /jednostki/3/rezerwacje       -- rezerwacje jednostki 3

⚠️ Nie zagnieżdżaj głębiej niż 2 poziomy (/a/1/b/2/c/3 jest już nieczytelne) — jeśli potrzebujesz głębszego zagnieżdżenia, rozważ osobny, płaski endpoint z filtrowaniem przez query params zamiast ścieżki.

3️⃣ Filtrowanie, sortowanie, paginacja — przez query params

GET /rezerwacje?status=zatwierdzona          -- filtrowanie
GET /rezerwacje?sortuj=data&kierunek=desc    -- sortowanie
GET /rezerwacje?strona=2&na_strone=20         -- paginacja

Query params (po ?) nadają się do opcjonalnych modyfikatorów zapytania — ścieżka (/rezerwacje) zostaje ta sama niezależnie od tego, jak filtrujesz czy sortujesz wynik.

NoteDlaczego paginacja jest ważna, nawet w małym projekcie?

GET /rezerwacje bez paginacji, gdy tabela urośnie do 100 000 wierszy, zwróci gigantyczną odpowiedź, która obciąży i serwer, i klienta. Dobra praktyka: zawsze projektuj endpointy listujące z myślą o paginacji, nawet jeśli na start danych jest mało.

4️⃣ Wersjonowanie API

Kiedy zmieniasz kształt API w sposób, który złamałby istniejących klientów (np. zmieniasz nazwę pola), potrzebujesz wersjonowania:

/api/v1/poligony
/api/v2/poligony

Alternatywa: wersjonowanie przez nagłówek (Accept: application/vnd.api.v2+json) — rzadziej spotykane w praktyce, ale czystsze koncepcyjnie. Nasz projekt jest na razie za mały, żeby potrzebować którejkolwiek z tych opcji naprawdę — zapamiętajcie obie jako narzędzia na później, gdy API będzie miało zewnętrznych klientów.

5️⃣ OpenAPI — dokumentacja blisko kodu

Pamiętacie /docs z pierwszego laboratorium? To nie przypadek, że działa automatycznie — FastAPI generuje specyfikację OpenAPI (JSON opisujący każdy endpoint, jego parametry, kształt odpowiedzi) bezpośrednio z Waszego kodu (adnotacji typów, modeli Pydantic). Dostępna jest pod /openapi.json.

Dlaczego to pomaga: dokumentacja pisana ręcznie (osobny plik Word/Markdown) zawsze z czasem rozjeżdża się z rzeczywistym kodem — ktoś zmienia endpoint i zapomina zaktualizować opis. Dokumentacja generowana z kodu jest bliżej prawdy, bo typy i nazwy parametrów nie mogą się rozjechać — to dosłownie ta sama adnotacja.

ImportantTo nie jest automat, o którym można zapomnieć

“Blisko kodu” nie znaczy “całkowicie odporne na błędy”. FastAPI wie o kodach błędów, które zwraca domyślnie (np. 422 z Pydantic) — ale własny HTTPException(404, ...) w ciele funkcji nie pojawi się w /docs, dopóki nie zadeklarujecie go jawnie w responses (przykład niżej). Zmienisz logikę błędu w kodzie i zapomnisz zaktualizować responses — i dokumentacja jednak się rozjedzie. “Generowane z kodu” zmniejsza ryzyko, nie eliminuje go.

@app.get(
    "/poligony/{poligon_id}",
    response_model=Poligon,
    summary="Pobierz jeden poligon po ID",
    description="Zwraca 404, jesli poligon o podanym ID nie istnieje.",
)
def pobierz_poligon(poligon_id: int):
    ...

Parametry summary/description trafiają bezpośrednio do /docs — dobra dokumentacja API to część kodu, nie osobny dokument.

6️⃣ Spójna struktura błędów

Zamiast każdy endpoint zwracający błędy w innym formacie, warto ustalić jeden wzorzec:

{
  "detail": "Poligon nie znaleziony"
}

FastAPI robi to domyślnie dla HTTPException (dokładnie ten kształt, który widzieliście już w laboratoriach) — trzymajcie się tego wzorca konsekwentnie we wszystkich endpointach.


🧾 Podsumowanie części I

  • URL opisuje zasób (rzeczownik, liczba mnoga), metoda HTTP opisuje akcję.
  • Zagnieżdżaj zasoby maksymalnie na 2 poziomy; filtruj/sortuj/paginuj przez query params.
  • Wersjonuj API (prefiks ścieżki albo nagłówek), gdy zmiana złamałaby istniejących klientów.
  • OpenAPI/Swagger generowane z kodu jest bliżej prawdy niż ręcznie pisana dokumentacja — ale własne kody błędów wciąż trzeba zadeklarować jawnie (responses).

🔒 Część II — Bezpieczeństwo aplikacji sieciowych

🎯 Cele

Poznasz różnicę między uwierzytelnianiem a autoryzacją, zrozumiesz jak działają tokeny JWT, oraz poznasz najważniejsze podatności z listy OWASP Top 10 i jak się przed nimi bronić — wiedzę, którą zastosujesz praktycznie w kolejnych laboratoriach.


1️⃣ Uwierzytelnianie vs autoryzacja

To dwa różne pytania, często mylone:

Pytanie Termin Przykład
Kim jesteś? Uwierzytelnianie (authentication) login + hasło, token
Co wolno Ci zrobić? Autoryzacja (authorization) planista może zatwierdzać rezerwacje, żołnierz — nie

System może wiedzieć, kim jesteś (uwierzytelniony), ale mimo to odmówić Ci operacji (brak autoryzacji) — to dokładnie różnica między 401 a 403 ze spotkania 1.

2️⃣ Jak przechowywać hasła — nigdy jawnym tekstem

# NIGDY:
if podane_haslo == uzytkownik.haslo:  # haslo w bazie jako czysty tekst

# ZAWSZE: hashowanie z "solą"
import bcrypt

hash_hasla = bcrypt.hashpw(haslo.encode(), bcrypt.gensalt())
# ... zapisz hash_hasla w bazie, NIGDY samego hasla

# przy logowaniu:
bcrypt.checkpw(podane_haslo.encode(), hash_hasla)

bcrypt (i podobne: argon2, scrypt) to funkcje jednokierunkowe — z hasha nie da się odtworzyć hasła, a każde wywołanie z “solą” (losowym dodatkiem) daje inny wynik nawet dla tego samego hasła, co chroni przed atakami tęczowych tablic (rainbow tables). Praktykę tę zastosujecie w kolejnych laboratoriach.

3️⃣ Sesje vs tokeny — dwa modele uwierzytelniania

Sesje (cookie-based) Tokeny (JWT)
Gdzie żyje stan po stronie serwera (baza sesji) w samym tokenie, po stronie klienta
Skalowanie na wiele serwerów wymaga współdzielonej bazy sesji trywialne — każdy serwer sam zweryfikuje token
Unieważnienie przed wygaśnięciem łatwe (usuń z bazy) trudniejsze (token jest ważny, dopóki nie wygaśnie)

W tym kursie używamy JWT (JSON Web Token) — pasuje do architektury bezstanowej ze spotkania 1.

4️⃣ Anatomia JWT (wariantu, którego używamy)

Standard JWT (RFC 7519) dopuszcza dwa warianty: JWS (JSON Web Signature — podpisany, ale czytelny) i JWE (JSON Web Encryption — faktycznie zaszyfrowany). W tym kursie, jak w zdecydowanej większości praktycznych API, używamy JWS — poniższy opis dotyczy tego wariantu, nie każdego możliwego tokenu JWT.

Token JWS to trzy części oddzielone kropkami: header.payload.signature

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYW5rb3dhbHNraSIsInJvbGEiOiJkb3dvZGNhIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header — jaki algorytm podpisu (np. HS256).
  • Payload — dane (np. {"sub": "jankowalski", "rola": "dowodca", "exp": 1735689600}) — czytelne dla każdego (to tylko Base64, nie szyfrowanie!), więc nigdy nie wkładajcie tam haseł czy innych sekretów.
  • Signature — podpis kryptograficzny, weryfikowany kluczem znanym tylko serwerowi — to on gwarantuje, że nikt nie sfałszował payloadu.
ImportantNasz JWT (JWS) jest podpisany, nie zaszyfrowany

Każdy, kto przechwyci token, może odczytać jego zawartość (Base64 to nie szyfrowanie) — nie może jej jednak zmienić niezauważenie, bo podpis by się nie zgadzał. Wniosek: JWS chroni przed fałszowaniem danych, nie przed ich odczytaniem — transport tokenu musi iść przez HTTPS, żeby nikt po drodze go nie podsłuchał. (JWE rozwiązałby też problem odczytu, kosztem większej złożoności — poza zakresem tego kursu.)

5️⃣ OWASP Top 10 — trzy podatności, które napotkacie w laboratoriach

SQL Injection

# PODATNE:
conn.execute(f"SELECT * FROM uzytkownicy WHERE login = '{login}'")
# atakujacy wpisuje: login = "' OR '1'='1"

Obrona: zawsze parametryzowane zapytania (widzieliście to już przy bazie danych) — conn.execute("... WHERE login = ?", (login,)).

XSS (Cross-Site Scripting)

Atakujący wstrzykuje złośliwy JavaScript do danych, które potem są wyświetlane innemu użytkownikowi bez odpowiedniego oczyszczenia (np. komentarz zawierający <script>...</script>, wyświetlony bez escapowania w przeglądarce ofiary).

Obrona: nowoczesne frameworki frontendowe (React i podobne) domyślnie escapują dane przy renderowaniu — problem pojawia się głównie, gdy ktoś świadomie to wyłączy (dangerouslySetInnerHTML i podobne).

CSRF (Cross-Site Request Forgery)

Atakujący nakłania przeglądarkę ofiary (zalogowanej w Waszej aplikacji) do wysłania żądania bez jej wiedzy (np. przez ukryty formularz na złośliwej stronie). Ponieważ przeglądarka automatycznie dołącza cookies do żądań, serwer “widzi” żądanie jako uwierzytelnione.

Obrona: API oparte o JWT w nagłówku Authorization (nie w cookies) jest z natury odporne na klasyczny CSRF — atakująca strona nie ma dostępu do tokenu przechowywanego przez Waszą aplikację (np. w pamięci JS), więc nie może go dołączyć do sfałszowanego żądania.

6️⃣ HTTPS/TLS — dlaczego zawsze

Bez TLS każdy w tej samej sieci (Wi-Fi w kawiarni, router po drodze) może odczytać cały ruch — łącznie z tokenami JWT i hasłami przesyłanymi przy logowaniu. TLS szyfruje transport między klientem a serwerem — to warunek konieczny, nie opcjonalny dodatek, dla każdej aplikacji przyjmującej dane logowania.


🧾 Podsumowanie części II

  • Uwierzytelnianie = kim jesteś; autoryzacja = co wolno Ci zrobić — to dwa osobne mechanizmy.
  • Hasła zawsze hashowane (bcrypt/argon2), nigdy przechowywane jawnym tekstem.
  • Nasz wariant JWT (JWS) jest podpisany (integralność), nie zaszyfrowany (poufność) — payload jest czytelny dla każdego; JWE istnieje, ale to poza zakresem kursu.
  • SQL injection, XSS, CSRF — trzy klasyczne podatności; parametryzowane zapytania, escapowanie i tokeny w nagłówku to podstawowa obrona.

📚 Źródła

  • Dokumentacja FastAPI — Path Operations, OpenAPI, Security
  • M. Masse, REST API Design Rulebook
  • OWASP Top 10 (owasp.org)
  • RFC 7519 — JSON Web Token (JWT)