JWT
JWT ist zu einer der wichtigsten Methoden für die moderne serverseitige Kommunikationsauthentifizierung geworden, mit den Eigenschaften leichtgewichtig und zustandslos.
Einführung
JWT steht für JSON Web Tokens. Eine detaillierte Einführung zu JWT finden Sie unter jwt.io. Es ist eine offene, sichere und kompakte Methode, um Informationen zwischen zwei Parteien als JSON-Objekt zu übertragen. Seine Eigenschaften sind hohe Sicherheit, Manipulationsschutz des Inhalts und geringer Ressourcenverbrauch.
Struktur
Im RFC-Standard besteht JWT aus folgenden drei Teilen:
- Header (Kopfteil)
- Payload (Nutzlast)
- Signature (Signatur)
Jeder Teil wird durch einen Punkt . getrennt und bildet schließlich eine Zeichenkette. Das Format ist header.payload.signature. Dies ist die Standardstruktur eines JWT-Tokens. Im Folgenden wird die Funktion jedes Teils erläutert.
TIP
Beachten Sie, dass base64 und base64URL nicht dieselben Kodierungsmethoden sind. Letztere ist kompatibel mit Web-URLs und führt entsprechende Escape-Sequenzen durch.
Header
Der Header deklariert nur einige grundlegende Informationen und besteht normalerweise aus zwei Teilen: dem Typ des Tokens und dem für die Signatur verwendeten Verschlüsselungsalgorithmus, zum Beispiel:
{
"alg": "HS256",
"typ": "JWT"
}Diese Informationen bedeuten im Wesentlichen: Der Token-Typ ist JWT, und der für die Signatur verwendete Verschlüsselungsalgorithmus ist HS256. Schließlich wird das JSON-Objekt durch Base64Url in eine Zeichenkette kodiert, die den Header des JWT bildet.
Payload
Der zweite Teil des JWT ist die Nutzlast, die hauptsächlich die Claims-Deklarationen enthält. Claims sind in der Regel Daten über eine Entität, wie z.B. einen Benutzer. Es gibt drei Arten von Claims:
registered:Registered claimsrepräsentieren einige vordefinierte Deklarationen, die nicht zwingend verwendet werden müssen, aber dennoch empfohlen werden, wie z.B.:iss(issuer, Aussteller),exp(expiration time, Ablaufzeit),aud(audience, Zielgruppe).public:Public claimskönnen von JWT-Benutzern frei definiert werden, sollten jedoch Konflikte mit anderen Claims vermeiden.private claims: Diese Claims sind ebenfalls benutzerdefiniert und werden normalerweise verwendet, um Informationen zwischen den beiden Parteien auszutauschen.
Ein Beispiel für eine Nutzlast:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}Dieses JSON-Objekt wird durch Base64Url in eine Zeichenkette kodiert und bildet den zweiten Teil des JWT.
DANGER
Obwohl der Nutzlastteil ebenfalls geschützt und gegen Manipulation gesichert ist, ist dieser Teil öffentlich lesbar. Speichern Sie daher keine sensiblen Informationen im JWT.
Signature
Nachdem der kodierte Header und die kodierte Nutzlast vorliegen, kann gemäß dem im Header angegebenen Signaturalgorithmus eine kryptografische Signatur basierend auf den Inhalten der ersten beiden Teile und dem Schlüssel erstellt werden. Wenn sich der Inhalt des JWT ändert, ist die bei der Entschlüsselung erhaltene Signatur anders. Bei Verwendung eines privaten Schlüssels kann auch der Aussteller des JWT verifiziert werden.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Ein Beispiel:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)Die endgültige Ausgabe ist eine Zeichenkette aus drei base64Url-Strings, getrennt durch ., die etwa so aussieht:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQFunktionsweise
Bei der Authentifizierung wird ein JSON Web Token zurückgegeben, wenn sich ein Benutzer erfolgreich mit seinen Anmeldedaten anmeldet. Da das Token ein Berechtigungsnachweis ist, muss sehr sorgfältig darauf geachtet werden, Sicherheitsprobleme zu vermeiden. Im Allgemeinen sollten Token nicht länger als notwendig gespeichert werden. Wann immer ein Benutzer auf geschützte Routen und Ressourcen zugreifen möchte, muss er das Token bei der Anfrage mitsenden, normalerweise im Authorization-Header mit dem Bearer-Schema, zum Beispiel:
Authorization: Bearer <token>Nach Erhalt des JWT überprüft der Server dessen Gültigkeit, z.B. ob der Inhalt manipuliert wurde, ob das Token abgelaufen ist usw. Wenn die Überprüfung erfolgreich ist, kann auf die Ressource zugegriffen werden. Obwohl JWT grundlegende Informationen enthalten kann, wird dennoch empfohlen, die Informationen nicht zu umfangreich zu gestalten.
JWT-Bibliothek
Offizielles Repository: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Offizielle Dokumentation: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Diese Bibliothek unterstützt das Parsen und Verifizieren sowie das Generieren und Signieren von JWT. Derzeit werden die Signaturalgorithmen HMAC SHA, RSA, RSA-PSS und ECDSA unterstützt, es können jedoch auch eigene Hooks hinzugefügt werden.
Installation
go get -u github.com/golang-jwt/jwt/v4Import
import "github.com/golang-jwt/jwt/v4"Wahl des Signaturalgorithmus
Es gibt verschiedene verfügbare Signaturalgorithmen. Vor der Verwendung sollten Sie die Unterschiede verstehen, um den richtigen Signaturalgorithmus zu wählen. Der größte Unterschied liegt zwischen symmetrischer Verschlüsselung und asymmetrischer Verschlüsselung.
Der einfachste symmetrische Verschlüsselungsalgorithmus HSA ermöglicht es jedem []byte als gültigen Schlüssel zu verwenden, daher ist die Berechnung etwas schneller. Der symmetrische Verschlüsselungsalgorithmus ist am effizientesten, wenn sowohl der Ersteller als auch der Verbraucher vertrauenswürdig sind. Da jedoch derselbe Schlüssel zum Signieren und Verifizieren verwendet wird, kann der zur Verifizierung verwendete Schlüssel nicht einfach verteilt werden, da derselbe Schlüssel auch zum Signieren verwendet wird. Wenn der Signaturschlüssel durchsickert, ist die Sicherheit des JWT bedeutungslos.
Asymmetrische Signaturmethoden wie RSA verwenden unterschiedliche Schlüssel zum Signieren und Verifizieren von Tokens. Dies ermöglicht das Generieren von Tokens mit einem privaten Schlüssel, während jeder mit dem öffentlichen Schlüssel normal verifizieren kann.
Verschiedene Signaturalgorithmen erfordern unterschiedliche Schlüsseltypen. Hier sind einige gängige Signaturalgorithmus-Typen:
HMAC: Symmetrische Verschlüsselung, erfordert einen Wert vom Typ[]bytezum Signieren und Verifizieren. (HS256,HS384,HS512)RSA: Asymmetrische Verschlüsselung, erfordert einen Wert vom Typ*rsa.PrivateKeyzum Signieren und einen Wert vom Typ*rsa.PublicKeyzum Verifizieren. (RS256,RS384,RS512)ECDSA: Asymmetrische Verschlüsselung, erfordert einen Wert vom Typ*ecdsa.PrivateKeyzum Signieren und einen Wert vom Typ*ecdsa.PublicKeyzum Verifizieren. (ES256,ES384,ES512)EdDSA: Asymmetrische Verschlüsselung, erfordert einen Wert vom Typed25519.PrivateKeyzum Signieren und einen Wert vom Typed25519.PublicKeyzum Verifizieren. (Ed25519)
Beispiele
Im Folgenden werden einige Beispiele demonstriert, die sich auf die Erstellung und Signierung sowie das Parsen und Verifizieren von JWT beziehen.
type Token struct {
Raw string // Rohes Token-String, wird beim Parsen gefüllt
Method SigningMethod // Zum Signieren verwendete Methode
Header map[string]interface{} // Header-Teil des JWT
Claims Claims // Payload-Teil des JWT
Signature string // Signatur-Teil des JWT, wird beim Parsen gefüllt
Valid bool // Ob das JWT gültig ist
}Die Token-Struktur repräsentiert ein JWT-Token. Die Verwendung der Felder hängt hauptsächlich davon ab, wie das JWT erstellt/signiert oder geparst/verifiziert wird.
type RegisteredClaims struct {
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
}Dies sind die in der Bibliothek vordefinierten Claims, die entsprechend verwendet werden können, um Anforderungen zu erfüllen.
Beispiel 1: HMAC-Erstellung und Signierung
func TestHmac(t *testing.T) {
// HMAC-Schlüsseltyp ist ein Byte-Array
secret := []byte("my secret")
// Verwende HS256-Algorithmus, jwt.MapClaims ist die Payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// Signieren
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Ausgabe:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Beispiel 2: Verwendung vordefinierter Claims
mySigningKey := []byte("AllYourBase")
// Claims erstellen
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)Ausgabe:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Beispiel 3: Benutzerdefinierte Claims
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Schlüssel erstellen
secret := []byte("my secret")
// Claims erstellen
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
// Token erstellen
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Signieren
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Ausgabe:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Wenn Sie benutzerdefinierte Claims mit eingebetteten Standard-Claims verwenden, stellen Sie sicher:
Die eingebetteten Standard-Claims sind Nicht-Zeiger-Typen
Wenn es sich um Zeiger-Typen handelt, stellen Sie sicher, dass vor der Übergabe geeigneter Speicher zugewiesen wird, sonst wird es zu einem Panic kommen.
Beispiel 4: HMAC Token parsen und verifizieren
func TestParse(t *testing.T) {
secret := []byte("my secret")
// Angenommen, ein Token wurde durch HS256-Algorithmus erstellt und signiert
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Token-String und Verifizierungs-Hook-Funktion übergeben, Rückgabewert ist eine Token-Struktur
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifizieren, ob der Signaturalgorithmus übereinstimmt
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Nicht übereinstimmender Signaturalgorithmus: %s", token.Header["alg"])
}
// Verifizierungsschlüssel zurückgeben
return secret, nil
})
if err != nil {
fmt.Println(token, err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Ausgabe:
map[id:123456 name:jack]Beispiel 5: Fehlerbehandlung
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// Angenommen, ein Token wurde durch HS256-Algorithmus erstellt und signiert
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Token-String und Verifizierungs-Hook-Funktion übergeben, Rückgabewert ist eine Token-Struktur
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifizieren, ob der Signaturalgorithmus übereinstimmt
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Nicht übereinstimmender Signaturalgorithmus: %s", token.Header["alg"])
}
// Verifizierungsschlüssel zurückgeben
return secret, nil
})
if token.Valid {
fmt.Println("Token ist gültig")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("Der übergebene String ist nicht einmal ein Token...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("Token ist abgelaufen oder noch nicht gültig")
} else {
fmt.Println("Token-Verarbeitungsfehler...")
}
}Ausgabe:
Token ist gültigBeispiel 6: Parsen mit benutzerdefinierten Claims
Wenn beim Erstellen des Tokens benutzerdefinierte Claims verwendet wurden und beim Parsen die Claims direkt in benutzerdefinierte Claims anstelle einer Map konvertiert werden sollen, müssen benutzerdefinierte Claims übergeben werden.
func TestCustomClaimsParse(t *testing.T) {
secret := []byte("my secret")
tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"})) // Option zur Verifizierung verwenden
// Typ-Assertion
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Ausgabe:
&{114514 {Server [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}Beispiel 7: RSA-Signierung und -Parsing
RSA wird häufiger in verteilten Architekturen verwendet. Der ungefähre Prozess ist wie folgt:
- Das Authentifizierungszentrum erstellt ein Schlüsselpaar, signiert das JWT mit dem privaten Schlüssel und gibt es an den Client zurück. Der öffentliche Schlüssel wird vom Geschäftsdienst gehalten.
- Der Client sendet eine Anfrage mit dem JWT an den Geschäftsdienst. Der Geschäftsdienst analysiert das JWT mit dem öffentlichen Schlüssel, ohne das Authentifizierungszentrum zu kontaktieren.
- Bei erfolgreicher Authentifizierung werden Geschäftsinformationen zurückgegeben.
- Bei fehlgeschlagener Authentifizierung werden Fehlerinformationen zurückgegeben.
func TestRsa(t *testing.T) {
// Schlüsselpaar erstellen
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
if err != nil {
fmt.Println(err)
return
}
// claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Mit privatem Schlüssel verschlüsseln
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Mit öffentlichem Schlüssel entschlüsseln
token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
fmt.Println(err)
} else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
}
}