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:
- 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.
- 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.