Wprowadzenie do JWT

JWT (JSON Web Token) to zwięzła, samowystarczalna metoda uwierzytelniania w sieci. Składa się z trzech części: nagłówka, ładunku i sygnatury. Nagłówek zawiera informacje o typie tokena i algorytmie szyfrowania, ładunek zawiera przekazywane dane (zwykle zawierające informacje o uprawnieniach i identyfikacji użytkownika), a sygnatura służy do weryfikacji integralności i poprawności tokenu.

JWT jest głównie używany w dwóch scenariuszach:

  1. Uwierzytelnianie: Po zalogowaniu użytkownika, każde kolejne żądanie będzie zawierać JWT, umożliwiając użytkownikowi dostęp do wyznaczonych tras, usług i zasobów.
  2. Wymiana informacji: JWT to dobry sposób bezpiecznej transmisji informacji między stronami, ponieważ mogą być cyfrowo podpisane, na przykład przy użyciu par klucza publicznego/prywatnego.

Format danych JWT

JWT to zwięzły, bezpieczny dla adresów URL sposób reprezentowania informacji do wymiany między stronami. JWT faktycznie składa się z trzech części, oddzielonych kropkami (.), a mianowicie Nagłówek.Ładunek.Sygnatura. Następnie szczegółowo omówimy format danych tych trzech części.

1. Nagłówek

Nagłówek zazwyczaj składa się z dwóch części: typu tokena – zazwyczaj JWT, i używanego algorytmu podpisu lub szyfrowania, takiego jak HMAC SHA256 lub RSA. Nagłówek jest reprezentowany w formacie JSON, a następnie zakodowany do postaci ciągu znaków za pomocą Base64Url. Na przykład:

{
  "alg": "HS256",
  "typ": "JWT"
}

Po zakodowaniu, możesz otrzymać ciąg znaków podobny do tego:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Ładunek

Ładunek składa się z serii twierdzeń (claims), które zawierają informacje o podmiocie (zwykle użytkowniku) i inne dane. Ładunek może zawierać wiele predefiniowanych twierdzeń (znanych również jako Predefiniowane Twierdzenia) oraz niestandardowe twierdzenia (Twierdzenia Prywatne).

Predefiniowane twierdzenia mogą zawierać:

  • iss (Issuer): Wystawca
  • exp (Expiration Time): Czas wygaśnięcia
  • sub (Subject): Temat
  • aud (Audience): Odbiorca
  • iat (Issued At): Czas wystawienia
  • nbf (Not Before): Czas wejścia w życie
  • jti (JWT ID): Unikalny identyfikator JWT

Przykładowy ładunek może wyglądać tak (w formacie JSON):

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

Te informacje również zostaną zakodowane za pomocą Base64Url, a możesz otrzymać ciąg znaków podobny do tego:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0

3. Sygnatura

Sekcja sygnatury służy do podpisania dwóch zakodowanych ciągów znaków wymienionych powyżej, w celu zweryfikowania, że wiadomość nie została naruszona podczas transmisji. Po pierwsze, musisz określić klucz (jeśli używasz algorytmu HMAC SHA256), a następnie użyć algorytmu określonego w nagłówku do podpisania nagłówka i ładunku.

Na przykład, jeśli masz następujący nagłówek i ładunek:

ZakodowanyNagłówek.ZakodowanyŁadunek

Pseudokod do podpisania ich przy użyciu klucza może być:

HMACSHA256(base64UrlEncode(nagłówek) + "." + base64UrlEncode(ładunek), tajnyKlucz)

Otrzymany podpisany ciąg znaków może wyglądać tak:

dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

Kompletny JWT

Połączenie tych trzech części za pomocą kropki (.) jako separatora tworzy kompletny JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

Standard JWT jest wysoce elastyczny i może przechowywać dowolne informacje potrzebne w ładunku (ale w celu zmniejszenia rozmiaru JWT i ze względów bezpieczeństwa, wrażliwe informacje nie powinny być przechowywane) oraz zapewnia integralność tych informacji poprzez sygnaturę.

Instalacja biblioteki Go JWT

Społeczność Go udostępnia pakiet github.com/golang-jwt/jwt do obsługi JWT. Instalacja tej biblioteki jest bardzo prosta, wystarczy uruchomić poniższą komendę w katalogu projektu:

go get -u github.com/golang-jwt/jwt/v5

Po zainstalowaniu można ją uwzględnić w sekcji import w następujący sposób:

import "github.com/golang-jwt/jwt/v5"

Tworzenie prostego tokena

Aby utworzyć prosty token JWT w języku Go z użyciem algorytmu HS256, należy wykonać następujące kroki:

Najpierw generujemy nowy obiekt Token, używając algorytmu HS256:

token := jwt.New(jwt.SigningMethodHS256)

Następnie używamy metody SignedString do wygenerowania ciągu znaków reprezentującego ten token, przekazując klucz, który zostanie użyty do podpisania.

var mySigningKey = []byte("twój-256-bitowy-sekret")
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Wystąpił błąd: %v", err)
}
fmt.Println(strToken)

To spowoduje wygenerowanie prostego tokenu bez żadnych twierdzeń.

Tworzenie tokenu z parametrami

Jedną z głównych funkcji JWT jest przenoszenie informacji. Ta informacja jest kodowana w Tokenie za pomocą twierdzeń. Na przykład, możemy utworzyć niestandardowe twierdzenia:

// Struktura niestandardowych twierdzeń
type MyClaims struct {
    jwt.RegisteredClaims
    NazwaUżytkownika string `json:"username"`
    Admin    bool   `json:"admin"`
}

// Utwórz token z niestandardowymi twierdzeniami
claims := MyClaims{
    RegisteredClaims: jwt.RegisteredClaims{},
    NazwaUżytkownika: "moja_nazwa_użytkownika",
    Admin: true,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Sekretny klucz do podpisania
var mySigningKey = []byte("twój-256-bitowy-sekret")

// Generuj token w postaci ciągu znaków
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Wystąpił błąd: %v", err)
}

fmt.Println(strToken)

W powyższym kodzie zdefiniowaliśmy strukturę MyClaims, aby zawierała zarejestrowane twierdzenia oraz dodatkowe niestandardowe informacje. Następnie nadal używamy SignedString do wygenerowania ciągu znaków reprezentującego token.

Parsowanie i weryfikacja tokenu

Parsowanie i weryfikacja JWT są bardzo ważne, i możesz to zrobić w następujący sposób:

// Będziemy używać tej samej struktury MyClaims
tokenString := "twój-ciąg-JWT"

// Musimy zdefiniować funkcję, którą pakiet jwt będzie używał do sparsowania tokenString
keyFunc := func(t *jwt.Token) (interface{}, error) {
    // Zweryfikuj, że użyty jest oczekiwany sposób podpisania
    if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("Niespodziewany sposób podpisania: %v", t.Header["alg"])
    }
    // Zwróć sekretny klucz tokena jwt w formacie []byte, zgodny z kluczem używanym wcześniej do podpisania
    return mySigningKey, nil
}

// Parsowanie tokenu
claims := &MyClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
    log.Fatalf("Błąd parsowania: %v", err)
}

if !parsedToken.Valid {
    log.Fatalf("Nieprawidłowy token")
}

// W tym momencie parsedToken został zweryfikowany, i możemy odczytać twierdzenia
fmt.Printf("Użytkownik: %s, Admin: %v\n", claims.NazwaUżytkownika, claims.Admin)

W powyższym kodzie podaliśmy ciąg znaków tokenu, instancję MyClaims oraz funkcję klucza keyFunc do funkcji jwt.ParseWithClaims. Ta funkcja zweryfikuje podpis i sparsuje token, wypełniając zmienną claims, jeśli token jest ważny.