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/3jest 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.
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.
“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.
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)