Dynamiczne trasy i uwierzytelnianie za pomocą ciasteczek w net/http: implementacja DeadDrop
W kontynuacji cyklu o czystym net/http implementujemy dynamiczne routowanie dla usługi DeadDrop. Użytkownicy będą mogli przeglądać sekrety za pomocą linków w formacie /secret/{id}. Standardowy mux nie obsługuje parametrów ścieżek do wersji Go 1.22, dlatego piszemy własny parser.
Handler rejestruje się na /secret/ z końcowym ukośnikiem — to przechwytuje wszystkie podrzędne ścieżki. Ręcznie wyodrębniamy ID z r.URL.Path.
func extractIDFromPath(path string) (string, bool) {
const prefix = "/secret/"
if !strings.HasPrefix(path, prefix) {
return "", false
}
id := strings.TrimPrefix(path, prefix)
if id == "" || strings.Contains(id, "/") {
return "", false
}
return id, true
}
Funkcja sprawdza prefiks, przycina go i wyklucza nieprawidłowe ID (puste lub z /). To podstawowa ochrona przed błędnymi żądaniami.
Handler SecretHandler z DI
Tworzymy strukturę-handler do wstrzykiwania zależności:
type SecretHandler struct {
// atrapa dla DI
}
func NewSecretHandler() *SecretHandler {
return &SecretHandler{}
}
func (h *SecretHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
id, ok := extractIDFromPath(r.URL.Path)
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Zażądano sekretu z ID: " + id))
}
Rejestrujemy w router.go: mux.Handle("/secret/", secretHandler). Testujemy przypadki brzegowe:
/secret/12345/→ 404/secret/→ 404/secret/axQ/233→ 404/secret/ad31H234→ OK z ID
Use Case dla pobierania sekretu
Definiujemy żądanie i odpowiedź:
type GetSecretRequest struct {
ID string
Password string
}
type GetSecretResponse struct {
ID string
Message string
FileName string
FileExt string
HasFile bool
ExpiresAt time.Time
}
Logika use case:
- Wyodrębniamy sekret z magazynu
- Sprawdzamy termin ważności (
time.After()), usuwamy przeterminowane - Weryfikujemy hasło przez
CheckPasswordHash - Zwracamy dane lub błąd
Uwierzytelnianie za pomocą ciasteczek
Aby chronić pobieranie plików, wprowadzimy uwierzytelnianie przez ciasteczka. Po pomyślnej weryfikacji hasła ustawiamy sesyjne ciasteczko z zaszyfrowanym tokenem dostępu.
- Zalety: bezstanowe, skalowalne, brak przechowywania sesji
- Parametry ciasteczka: HttpOnly, Secure, SameSite=Strict
Przy żądaniu /download/{id}:
- Wyodrębniamy ID ze ścieżki
- Odczytujemy ciasteczko
auth_token - Dekodujemy i sprawdzamy token (ID + hash hasła + nonce)
- Przy ważności — udostępniamy plik
Struktura tokenu dostępu
Token tworzony jest jako base64(ID + ":" + hash + ":" + nonce), gdzie:
hash = HMAC-SHA256(ID + password, secretKey)nonce— jednorazowy numer do ochrony przed atakami replay
Przykład walidacji:
func validateDownloadToken(id, token string) bool {
// dekodujemy token
// wyodrębniamy hash i nonce
// przeliczamy HMAC
// porównujemy
return true // lub false
}
Pełny cykl pracy z sekretem
- Tworzenie: POST
/create→ generujemy ID, haszujemy hasło, zapisujemy - Przeglądanie: GET
/secret/{id}→ formularz wprowadzania hasła - Uwierzytelnianie: POST hasło → ciasteczko + przekierowanie
- Pobieranie: GET
/download/{id}→ sprawdzenie ciasteczka → plik - Czyszczenie: TTL wygasł → usunięcie
Prototyp UI: strona z polem hasła, przycisk "Otwórz". Po sukcesie — zawartość + link do pliku.
Testowanie routingu
| Ścieżka testowa | Oczekiwanie | Wynik |
|---------------|----------|-----------|
| /secret/123/ | 404 | 404 |
| /secret/ | 404 | 404 |
| /secret/id/with/slash | 404 | 404 |
| /secret/valid123 | OK: "ID: valid123" | OK |
Przypadki brzegowe pokrywają bezpieczeństwo parsera.
Co jest ważne
- Dynamiczne trasy: ręczne parsowanie
r.URL.Pathbez zewnętrznych routerów - Bezpieczeństwo: walidacja ID, sprawdzanie ukośników, TTL dla sekretów
- Uwierzytelnianie: ciasteczko z tokenem HMAC zamiast sesji
- Architektura: use case + DI, czysta logika poza handlerem
- Skalowalność: bezstanowe, gotowe do klastra
— Editorial Team
Brak komentarzy.