Go dla początkujących - Część 15: Sesje i uwierzytelnianie
Mateusz Kędziora

Witaj w kolejnym artykule z serii Go dla początkujących! Dzisiaj zajmiemy się kluczowym aspektem tworzenia bezpiecznych aplikacji internetowych: sesjami i autentykacją użytkowników. To zagadnienia fundamentalne, jeśli chcesz, aby twoja aplikacja potrafiła odróżniać poszczególnych użytkowników i chronić ich dane.
Wprowadzenie do Bezpieczeństwa w Aplikacjach Web
Kiedy tworzysz aplikację internetową, ważne jest, aby zrozumieć, że cała komunikacja odbywa się przez protokół HTTP. HTTP jest protokołem bezstanowym, co oznacza, że serwer nie pamięta niczego o poprzednich żądaniach od tego samego użytkownika. To świetne dla skalowalności, ale stwarza problem: jak rozpoznać użytkownika, który już się zalogował? Odpowiedzią są sesje i autentykacja.
- Autentykacja (uwierzytelnianie) to proces weryfikacji tożsamości użytkownika. Najczęściej robi się to, sprawdzając, czy podane hasło zgadza się z tym, co jest zapisane w bazie danych.
- Sesja to sposób na “zapamiętanie” zalogowanego użytkownika między kolejnymi żądaniami HTTP. Serwer generuje unikalny identyfikator (ID sesji) i przekazuje go do klienta (przeglądarki). Klient wysyła ten ID sesji z każdym żądaniem, pozwalając serwerowi rozpoznać użytkownika.
Sesje w Go z Użyciem gorilla/sessions
gorilla/sessions
to popularna biblioteka w Go, która upraszcza zarządzanie sesjami. Zapewnia wygodny interfejs do tworzenia, odczytywania i usuwania sesji.
Instalacja:
Najpierw zainstaluj bibliotekę:
go get github.com/gorilla/sessions
Przykład Implementacji Sesji:
Poniższy kod demonstruje, jak utworzyć sesję, zapisać w niej dane użytkownika (np. jego ID) i odczytać te dane przy kolejnym żądaniu.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/sessions"
)
var (
// Klucz używany do szyfrowania ciasteczek sesji. Zmień go na prawdziwy klucz w środowisku produkcyjnym!
key = []byte("bardzo-tajny-klucz") //Zmień to!
store = sessions.NewCookieStore(key)
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
// Załóżmy, że sprawdziliśmy dane logowania i są poprawne
userID := "user123"
session, _ := store.Get(r, "session-name") // "session-name" to nazwa ciasteczka
// Ustaw dane użytkownika w sesji
session.Values["userID"] = userID
err := session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Zalogowano! ID sesji zapisane w ciasteczku.")
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
// Odczytaj dane użytkownika z sesji
userID, ok := session.Values["userID"].(string) // Type Assertion
if !ok {
fmt.Fprintln(w, "Niezalogowany")
return
}
fmt.Fprintf(w, "Witaj, %s!\n", userID)
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
session.Options.MaxAge = -1 // Natychmiastowe usunięcie ciasteczka
err := session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Wylogowano!")
}
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/", homeHandler)
http.HandleFunc("/logout", logoutHandler)
fmt.Println("Serwer uruchomiony na porcie 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Wyjaśnienie Kodu:
- Import bibliotek: Importujemy potrzebne pakiety, w tym
net/http
do obsługi żądań HTTP orazgithub.com/gorilla/sessions
do obsługi sesji. - Konfiguracja sesji:
key
: To klucz używany do szyfrowania danych sesji. Bardzo ważne: w środowisku produkcyjnym użyj losowo wygenerowanego, silnego klucza. Nigdy nie używaj takiego prostego klucza jak w tym przykładzie!store
: Tworzy instancjęCookieStore
, która przechowuje dane sesji w ciasteczkach (cookies) po stronie klienta. Alternatywą jestFileSystemStore
, który przechowuje sesje na serwerze (ale wymaga dodatkowej konfiguracji i zarządzania).
loginHandler
:- Pobiera sesję za pomocą
store.Get(r, "session-name")
.session-name
to nazwa ciasteczka, w którym przechowywany jest identyfikator sesji. - Ustawia wartość
"userID"
w sesji na identyfikator zalogowanego użytkownika. - Zapisuje sesję za pomocą
session.Save(r, w)
. To powoduje wysłanie ciasteczka z ID sesji do przeglądarki użytkownika.
- Pobiera sesję za pomocą
homeHandler
:- Pobiera sesję.
- Odczytuje wartość
"userID"
z sesji za pomocąsession.Values["userID"].(string)
. Zauważ, że potrzebna jest type assertion.(string)
aby przekonwertować interfejs (typ danych w mapiesession.Values
) na string. - Wyświetla powitanie dla zalogowanego użytkownika lub informację o braku logowania.
logoutHandler
:- Pobiera sesję.
- Ustawia
MaxAge
na -1 co powoduje natychmiastowe usunięcie ciasteczka z sesją. - Zapisuje sesję.
main
:- Definiuje routing dla
loginHandler
ihomeHandler
. - Uruchamia serwer HTTP na porcie 8080.
- Definiuje routing dla
Uruchomienie przykładu:
- Zapisz kod jako
main.go
. - Uruchom:
go run main.go
- Otwórz w przeglądarce:
http://localhost:8080/login
(zalogowanie)http://localhost:8080/
(strona główna, wyświetlenie informacji o zalogowaniu)http://localhost:8080/logout
(wylogowanie)
Sprawdź ciasteczka w swojej przeglądarce po zalogowaniu. Powinieneś zobaczyć ciasteczko o nazwie session-name
.
Ważne kwestie dotyczące bezpieczeństwa sesji:
- Szyfrowanie sesji: Upewnij się, że używasz silnego klucza do szyfrowania danych sesji. Klucz powinien być przechowywany w bezpiecznym miejscu (np. w zmiennej środowiskowej).
- HTTPS: Koniecznie używaj HTTPS (SSL/TLS) dla swojej aplikacji. Bez HTTPS, ciasteczka z ID sesji mogą być przechwycone przez atakującego.
HttpOnly
flag: Ustaw flagęHttpOnly
dla ciasteczek sesji. Zapobiega to dostępowi do ciasteczek przez JavaScript, co utrudnia ataki XSS (Cross-Site Scripting). Wgorilla/sessions
możesz to zrobić konfigurującsession.Options.HttpOnly = true
.Secure
flag: Ustaw flagęSecure
dla ciasteczek sesji. Powoduje to, że ciasteczko jest wysyłane tylko przez połączenie HTTPS. Wgorilla/sessions
możesz to zrobić konfigurującsession.Options.Secure = true
.- Limit czasu trwania sesji: Ustaw limit czasu trwania sesji (np. 30 minut). Po upływie tego czasu sesja powinna automatycznie wygasnąć. W
gorilla/sessions
możesz to zrobić konfigurującsession.Options.MaxAge = 30 * 60
(czas w sekundach). - Regeneracja ID sesji: Po zalogowaniu i wylogowaniu użytkownika, regeneruj ID sesji. Zapobiega to atakom typu session fixation. W
gorilla/sessions
możesz to zrobić wywołującsession.ID = ""
i następniesession.Save(r, w)
.
Autentykacja Użytkowników
Teraz zajmiemy się autentykacją, czyli procesem weryfikacji tożsamości użytkownika.
1. Hasła:
Najpopularniejszą metodą autentykacji jest użycie hasła. Jednak przechowywanie haseł w sposób jawny jest bardzo niebezpieczne. Zawsze należy używać funkcji haszujących (np. bcrypt) do przechowywania haseł.
Przykład użycia bcrypt
:
package main
import (
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), nil
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "MojeBardzoSilneHaslo123!"
// Haszowanie hasła
hashedPassword, err := hashPassword(password)
if err != nil {
log.Fatal(err)
}
fmt.Println("Zahasowane hasło:", hashedPassword)
// Sprawdzanie hasła
match := checkPasswordHash(password, hashedPassword)
fmt.Println("Czy hasła pasują:", match)
wrongPassword := "ZleHaslo"
match = checkPasswordHash(wrongPassword, hashedPassword)
fmt.Println("Czy hasła (złe) pasują:", match)
}
Wyjaśnienie Kodu:
- Import: Importujemy pakiet
golang.org/x/crypto/bcrypt
. hashPassword
:bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
haszuje hasło.bcrypt.DefaultCost
określa siłę haszowania. Wyższa wartość oznacza większe bezpieczeństwo, ale też dłuższy czas obliczeń.- Funkcja zwraca zahaszowane hasło jako string i ewentualny błąd.
checkPasswordHash
:bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
porównuje podane hasło z zahaszowanym hasłem.- Funkcja zwraca
true
jeśli hasła pasują,false
w przeciwnym wypadku.
Najlepsze praktyki dotyczące haseł:
- Używaj silnych haseł: Hasła powinny być długie (przynajmniej 12 znaków), zawierać kombinację liter (dużych i małych), cyfr i znaków specjalnych.
- Nie przechowuj haseł jawnie: Zawsze haszuj hasła przed zapisaniem ich w bazie danych.
- Używaj
bcrypt
lub podobnych algorytmów:bcrypt
jest odporny na ataki typu brute-force i rainbow table. - Salt:
bcrypt
automatycznie dodaje losowy salt (sól) do hasła przed haszowaniem. Salt zapobiega atakom typu rainbow table. - Ogranicz liczbę nieudanych prób logowania: Po kilku nieudanych próbach logowania, zablokuj konto na pewien czas. Zapobiega to atakom brute-force.
- Wymuszaj regularne zmiany haseł: Zalecaj użytkownikom regularną zmianę haseł.
- Pamiętaj o resetowaniu haseł: Zaimplementuj mechanizm resetowania haseł, który pozwala użytkownikom na odzyskanie dostępu do konta, jeśli zapomną hasła.
2. Tokeny JWT (JSON Web Tokens):
JWT to standard branżowy służący do bezpiecznego przesyłania informacji między stronami jako obiekt JSON. JWT są często używane do autoryzacji, szczególnie w systemach API.
Zalety JWT:
- Bezstanowość: Serwer nie musi przechowywać informacji o sesji. Informacje są zawarte w samym tokenie.
- Skalowalność: Łatwiej skalować aplikacje, które używają JWT, ponieważ serwer nie musi utrzymywać sesji.
- Mobilność: JWT mogą być używane do autentykacji na różnych platformach (web, mobile, desktop).
Przykład implementacji JWT:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
var jwtKey = []byte("BardzoTajnyKluczJWT") // Zmień to!
type Claims struct {
UserID string `json:"userID"`
jwt.StandardClaims
}
func generateJWT(userID string) (string, error) {
expirationTime := time.Now().Add(5 * time.Minute) // Token ważny 5 minut
claims := &Claims{
UserID: userID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
Issuer: "moja-aplikacja",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
func authenticateJWT(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("Nieprawidłowy token")
}
return claims, nil
}
func loginHandlerJWT(w http.ResponseWriter, r *http.Request) {
userID := "user123" // Załóżmy, że użytkownik został zweryfikowany
token, err := generateJWT(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "JWT token: %s\n", token)
}
func homeHandlerJWT(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization") // Pobieranie tokenu z nagłówka Authorization
if tokenString == "" {
http.Error(w, "Brak tokenu Authorization", http.StatusUnauthorized)
return
}
claims, err := authenticateJWT(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Witaj, %s! (JWT)\n", claims.UserID)
}
func main() {
http.HandleFunc("/login-jwt", loginHandlerJWT)
http.HandleFunc("/home-jwt", homeHandlerJWT)
fmt.Println("Serwer uruchomiony na porcie 8080 (JWT)")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Wyjaśnienie Kodu:
- Import: Importujemy pakiety, w tym
github.com/dgrijalva/jwt-go
. jwtKey
: Klucz używany do podpisywania tokenów JWT. Bardzo ważne: w środowisku produkcyjnym użyj losowo wygenerowanego, silnego klucza. Nigdy nie używaj takiego prostego klucza jak w tym przykładzie! Klucz powinien być przechowywany bezpiecznie.Claims
: Struktura reprezentująca dane (claims) zawarte w tokenie JWT. ZawieraUserID
oraz standardowe pola JWT (np.ExpiresAt
,Issuer
).generateJWT
:- Tworzy obiekt
Claims
z danymi użytkownika i czasem wygaśnięcia tokenu. - Tworzy token JWT za pomocą
jwt.NewWithClaims
. - Podpisuje token za pomocą
token.SignedString(jwtKey)
. - Zwraca token JWT jako string.
- Tworzy obiekt
authenticateJWT
:- Parsuje token JWT za pomocą
jwt.ParseWithClaims
. - Sprawdza, czy token jest ważny za pomocą
token.Valid
. - Zwraca obiekt
Claims
z danymi zawartymi w tokenie.
- Parsuje token JWT za pomocą
loginHandlerJWT
:- Generuje token JWT dla użytkownika.
- Wysyła token do klienta.
homeHandlerJWT
:- Pobiera token JWT z nagłówka
Authorization
. - Uwierzytelnia token za pomocą
authenticateJWT
. - Wyświetla powitanie dla użytkownika.
- Pobiera token JWT z nagłówka
main
:- Definiuje routing.
Uruchomienie przykładu:
- Zapisz kod jako
main.go
. - Zainstaluj bibliotekę JWT:
go get github.com/dgrijalva/jwt-go
- Uruchom:
go run main.go
- Otwórz w przeglądarce lub użyj narzędzia takiego jak Postman:
http://localhost:8080/login-jwt
(otrzymanie tokenu JWT)http://localhost:8080/home-jwt
(żądanie do strony chronionej, wymaga nagłówkaAuthorization
z tokenem JWT) Ustaw nagłówekAuthorization
na wartośćtoken: <otrzymany token JWT>
.
Ważne kwestie dotyczące bezpieczeństwa JWT:
- Silny klucz: Używaj silnego i losowo wygenerowanego klucza do podpisywania tokenów.
- Przechowywanie klucza: Przechowuj klucz w bezpiecznym miejscu.
- HTTPS: Używaj HTTPS do przesyłania tokenów.
- Krótki czas życia tokenu: Ustaw krótki czas życia tokenu (np. 5-15 minut).
- Odświeżanie tokenów: Zaimplementuj mechanizm odświeżania tokenów, aby użytkownik nie musiał logować się ponownie po wygaśnięciu tokenu.
- Walidacja tokenów: Zawsze waliduj tokeny po stronie serwera.
Praca Domowa
- Rozwiń przykład z sesjami, dodając formularz logowania i rejestracji użytkownika. Hasła przechowuj z użyciem
bcrypt
. - Zaimplementuj mechanizm resetowania hasła w przykładzie z hasłami.
- Spróbuj użyć
FileSystemStore
zamiastCookieStore
w przykładzie z sesjami. Jakie są zalety i wady takiego rozwiązania? - Dodaj middleware, który będzie sprawdzał ważność tokenu JWT dla wszystkich endpointów wymagających autoryzacji.
- Poczytaj o metodach autentykacji 2FA (Two-Factor Authentication). Czy mógłbyś zaimplementować prosty 2FA w swojej aplikacji?
Podsumowanie
W tym artykule omówiliśmy podstawy bezpieczeństwa aplikacji Go, skupiając się na sesjach i autentykacji użytkowników. Pokazaliśmy, jak używać biblioteki gorilla/sessions
do zarządzania sesjami oraz jak implementować autentykację za pomocą haseł (z użyciem bcrypt
) i tokenów JWT. Pamiętaj, że bezpieczeństwo to proces ciągły. Zawsze bądź na bieżąco z najnowszymi zagrożeniami i najlepszymi praktykami.
Zachęcam do dalszego eksperymentowania z kodem, modyfikowania go i dopasowywania do własnych potrzeb. To najlepszy sposób na naukę. Nie zapomnij również przeczytać pozostałych artykułów z serii Go dla początkujących!
Przydatne Źródła
- Oficjalna dokumentacja języka Go: https://golang.org/doc/
- Tutoriale Go: https://golang.org/doc/tutorial/
gorilla/sessions
: https://github.com/gorilla/sessionsbcrypt
: https://godoc.org/golang.org/x/crypto/bcryptjwt-go
: https://github.com/dgrijalva/jwt-go (Uwaga: Ta biblioteka jest w archiwum. Rozważ użycie nowszej biblioteki np.github.com/golang-jwt/jwt/v5
)- OWASP (Open Web Application Security Project): https://owasp.org/ - Świetne źródło wiedzy o bezpieczeństwie aplikacji webowych.
Życzę powodzenia w dalszej nauce Go i tworzeniu bezpiecznych aplikacji!
Polecane artykuły
Docker vs Kubernetes: Który dla Ciebie w 2025?
Docker i Kubernetes objaśnione! Która technologia lepsza dla początkujących w 2025? Porównanie, przykłady i przyszłość.
Mateusz Kędziora
DevOps: Automatyzacja zadań sysadmina dla programistów
Zautomatyzuj pracę sysadmina w środowisku DevOps! Praktyczne przykłady, skrypty, Ansible, Terraform, Prometheus i Grafana.
Mateusz Kędziora
Automatyzacja Linux/macOS z Bash: Praktyczny Przewodnik
Zacznij automatyzować system Linux/macOS z Bash! Dowiedz się, czym jest Bash, jak pisać skrypty i używać podstawowych komend.
Mateusz Kędziora