JWT in Aktion: Sichere Authentifizierung & Autorisierung in Go
Lukas Schneider
DevOps Engineer · Leapcell

Vertiefende Erklärung von JWT: Prinzipien, Format, Funktionen und Anwendung in Go-Projekten
Was ist JWT
JWT ist die Abkürzung für JSON Web Token, eine domänenübergreifende Authentifizierungslösung. Es spielt eine entscheidende Rolle in Webanwendungen und ermöglicht eine sichere und komfortable Authentifizierung und Informationsübertragung.
Probleme, die durch die Verwendung von JWT gelöst werden
Einschränkungen des traditionellen Benutzerauthentifizierungsprozesses
Die traditionelle Authentifizierung basiert auf clientseitigen Cookies und serverseitigen Sitzungen. Dies funktioniert gut für Einzelserveranwendungen. Bei Multi-Server-Bereitstellungen gibt es jedoch ein Problem mit der Sitzungsfreigabe. In einem groß angelegten verteilten System, in dem mehrere Server zusammenarbeiten, verwaltet jeder Server eine unabhängige Sitzung. Wenn ein Benutzer zwischen Servern wechselt, kann es zu inkonsistenten Anmeldezuständen kommen. Gleichzeitig kann Single-Sign-On nicht über Cookie + Session über verschiedene Domänen hinweg erreicht werden, da Cookies domänenbasiert gesetzt werden und verschiedene Domänen Cookies nicht direkt gemeinsam nutzen können, was die einheitliche Authentifizierung und Zugriffskontrolle in Multi-Business-Systemen einschränkt.
Vorteile von JWT
JWT macht Anwendungen zustandslos und vermeidet die Notwendigkeit der Sitzungsfreigabe. Es enthält Benutzerinformationen innerhalb seiner eigenen Struktur. Server müssen keine Sitzungen speichern. Stattdessen können sie die Identität des Benutzers bestätigen, indem sie die Gültigkeit des JWT für jede Anfrage überprüfen. In einem verteilten System erleichtert JWT die Servererweiterung und wird nicht durch die Anzahl und Verteilung der Server beeinträchtigt.
Format von JWT
Ein korrektes JWT-Format ist wie folgt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZaYjiJ9._eCVNYFYnMXwpgGX9Iu412EQSOFuEGl2c
Wie zu sehen ist, besteht eine JWT-Zeichenkette aus drei Teilen: Header, Payload und Signatur, die durch Punkte verbunden sind.
Header
Der Header ist ein JSON-Objekt, das aus zwei Teilen besteht: dem Token-Typ und dem Verschlüsselungsalgorithmus. Zum Beispiel:
{ "typ": "JWT",// Normalerweise "JWT" "alg": "HS256"// Unterstützt verschiedene Verschlüsselungsalgorithmen }
Konvertieren Sie das obige JSON-Objekt mit dem Base64URL-Algorithmus in eine Zeichenkette, um den Header-Teil des JWT zu erhalten. Es ist zu beachten, dass die JWT-Kodierung nicht standardmäßiges Base64 verwendet, sondern Base64Url. Dies liegt daran, dass in der von Base64 generierten Zeichenkette drei Sonderzeichen in URLs enthalten sein können: +, / und =. Und wir können das Token auf der URL übergeben (z. B. test.com?token = xxx). Der Base64URL-Algorithmus lässt auf der Grundlage der von dem Base64-Algorithmus generierten Zeichenkette das = weg, ersetzt + durch - und ersetzt / durch _. Dies stellt sicher, dass die generierte Zeichenkette problemlos in der URL übergeben werden kann.
Payload
Der Payload-Teil des JWT ist wie der Header auch ein JSON-Objekt, das zum Speichern der tatsächlichen Daten dient, die wir benötigen. Der JWT-Standard bietet sieben optionale Felder, nämlich:
- iss(issuer): Der Aussteller, dessen Wert eine Zeichenkette oder ein Uri ist, bei der die Groß- und Kleinschreibung beachtet wird.
- sub(subject): Das Subjekt, das zur Identifizierung eines Benutzers verwendet wird.
- exp(expiration time): Die Ablaufzeit.
- aud(audience): Die Zielgruppe.
- iat(issued at): Die Ausstellungszeit.
- nbf(not before): Die Zeit, vor der das JWT nicht gültig ist.
- jti(JWT ID): Die Kennung.
Zusätzlich zu den Standardfeldern können wir bei Bedarf private Felder definieren, um Geschäftsanforderungen zu erfüllen. Zum Beispiel:
{ iss:"admin",// Standardfeld jti:"test",// Standardfeld username:"leapcell",// Benutzerdefiniertes Feld "gender":"male", "avatar":"https://avatar.leapcell.jpg" }
Konvertieren Sie das obige JSON-Objekt mit dem Base64URL-Algorithmus in eine Zeichenkette, um den Payload-Teil des JWT zu erhalten.
Signatur
Die Signatur ist die Signatur des JWT. Die Erzeugungsmethode ist wie folgt: Kodieren Sie den Header und den Payload mit dem Base64URL-Algorithmus, verbinden Sie sie mit einem Punkt und verschlüsseln Sie sie dann mit dem geheimen Schlüssel (secretKey) und der im Header angegebenen Verschlüsselungsmethode, um schließlich die Signatur zu erzeugen. Die Rolle der Signatur besteht darin, sicherzustellen, dass das JWT während der Übertragung nicht manipuliert wurde. Der Server kann die Integrität und Authentizität des JWT überprüfen, indem er die Signatur verifiziert.
Funktionen von JWT
- Sicherheitsempfehlung: Es ist am besten, das HTTPS-Protokoll zu verwenden, um die Möglichkeit eines JWT-Diebstahls zu verhindern. Da die Datenübertragung unter dem HTTP-Protokoll im Klartext erfolgt, ist es einfach, sie abzufangen und zu manipulieren. HTTPS kann die Sicherheit von JWT durch verschlüsselte Übertragung wirksam schützen.
- Einschränkung des Invalidierungsmechanismus: Mit Ausnahme des Ablaufs der Ausstellungszeit des JWT gibt es keine andere Möglichkeit, ein bereits generiertes JWT zu invalidieren, es sei denn, die Serverseite ändert den Algorithmus. Dies bedeutet, dass ein JWT, wenn es innerhalb der Gültigkeitsdauer gestohlen wird, missbräuchlich verwendet werden kann.
- Speicherung sensibler Informationen: Wenn das JWT nicht verschlüsselt ist, sollten keine sensiblen Informationen darin gespeichert werden. Wenn sensible Informationen gespeichert werden müssen, ist es am besten, sie erneut zu verschlüsseln. Da das JWT selbst dekodiert werden kann. Wenn es sensible Informationen enthält und nicht verschlüsselt ist, besteht ein Sicherheitsrisiko.
- Einstellung der Ablaufzeit: Es ist ratsam, eine kurze Ablaufzeit für das JWT festzulegen, um zu verhindern, dass es bei Diebstahl gültig bleibt, wodurch potenzielle Verluste reduziert werden. Eine kurze Ablaufzeit kann das Risiko verringern, nachdem das JWT gestohlen wurde. Selbst wenn es gestohlen wird, ist seine Gültigkeitsdauer begrenzt.
- Speicherung von Geschäftsinformationen: Der Payload des JWT kann auch einige Geschäftsinformationen speichern, wodurch Datenbankabfragen reduziert werden können. So können beispielsweise grundlegende Benutzerinformationen im Payload gespeichert werden. Jedes Mal, wenn eine Anfrage gestellt wird, kann der Server diese Informationen direkt aus dem JWT beziehen, ohne die Datenbank erneut abzufragen, wodurch die Leistung und die Reaktionsgeschwindigkeit des Systems verbessert werden.
Verwendung von JWT
Nachdem der Server das JWT ausgestellt hat, sendet er es an den Client. Wenn der Client ein Browser ist, kann er in einem Cookie oder im localStorage gespeichert werden. Wenn es sich um eine APP handelt, kann sie in einer SQLite-Datenbank gespeichert werden. Dann wird für jede Schnittstellenanfrage das JWT übertragen. Es gibt viele Möglichkeiten, es an die Serverseite zu übertragen, z. B. Query, Cookie, Header oder Body. Kurz gesagt, jede Möglichkeit, Daten an den Server zu übertragen, kann verwendet werden. Der standardisiertere Ansatz besteht jedoch darin, es über den Header Authorization hochzuladen, und zwar im folgenden Format:
Authorization: Bearer <token>
Diese Art der Übergabe des JWT im HTTP-Request-Header entspricht gängigen Authentifizierungsspezifikationen und ist für den Server zur Durchführung einer einheitlichen Authentifizierungsverarbeitung geeignet.
Verwendung von JWT in Go-Projekten
Generieren von JWT
Verwenden Sie die Bibliothek github.com/golang-jwt/jwt
, um uns beim Generieren oder Analysieren von JWT zu helfen. Wir können die Methode NewWithClaims()
verwenden, um ein Token-Objekt zu erzeugen, und dann die Methode des Token-Objekts verwenden, um eine JWT-Zeichenkette zu erzeugen. Zum Beispiel:
package main import ( "fmt" "time" "github.com/golang-jwt/jwt" ) func main() { hmacSampleSecret := []byte("123")// Geheimer Schlüssel, darf nicht preisgegeben werden // Generieren eines Token-Objekts token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "foo": "bar", "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), }) // Generieren einer jwt-Zeichenkette tokenString, err := token.SignedString(hmacSampleSecret) fmt.Println(tokenString, err) }
Wir können auch die Methode New()
verwenden, um ein Token-Objekt zu erzeugen und dann eine JWT-Zeichenkette zu erzeugen. Zum Beispiel:
package main import ( "fmt" "time" "github.com/golang-jwt/jwt" ) func main() { hmacSampleSecret := []byte("123") token := jwt.New(jwt.SigningMethodHS256) // Daten können nicht übertragen werden, wenn sie über die Methode New erstellt werden, so dass Daten durch Zuweisen von Werten zu token.Claims definiert werden können token.Claims = jwt.MapClaims{ "foo": "bar", "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), } tokenString, err := token.SignedString(hmacSampleSecret) fmt.Println(tokenString, err) }
In den obigen Beispielen werden die Daten im Payload des JWT über die Datenstruktur jwt.MapClaims
definiert. Zusätzlich zur Verwendung von jwt.MapClaims
können wir auch eine benutzerdefinierte Struktur verwenden. Diese Struktur muss jedoch die folgende Schnittstelle implementieren:
type Claims interface { Valid() error }
Das Folgende ist ein Beispiel für die Implementierung einer benutzerdefinierten Datenstruktur:
package main import ( "fmt" "github.com/golang-jwt/jwt" ) type CustomerClaims struct { Username string `json:"username"` Gender string `json:"gender"` Avatar string `json:"avatar"` Email string `json:"email"` } func (c CustomerClaims) Valid() error { return nil } func main() { // Geheimer Schlüssel hmacSampleSecret := []byte("123") token := jwt.New(jwt.SigningMethodHS256) token.Claims = CustomerClaims{ Username: "Leapcell", Gender: "male", Avatar: "https://avatar.leapcell.jpg", Email: "admin@test.org", } tokenString, err := token.SignedString(hmacSampleSecret) fmt.Println(tokenString, err) }
Wenn wir die im JWT-Standard definierten Felder in der benutzerdefinierten Struktur verwenden wollen, können wir dies wie folgt tun:
type CustomerClaims struct { *jwt.StandardClaims// Standardfelder Username string `json:"username"` Gender string `json:"gender"` Avatar string `json:"avatar"` Email string `json:"email"` }
Parsen von JWT
Das Parsen ist die umgekehrte Operation der Generierung. Wir parsen ein Token, um seinen Header und Payload zu erhalten und zu überprüfen, ob die Daten durch die Signatur manipuliert wurden. Das Folgende ist die spezifische Implementierung:
package main import ( "fmt" "github.com/golang-jwt/jwt" ) type CustomerClaims struct { Username string `json:"username"` Gender string `json:"gender"` Avatar string `json:"avatar"` Email string `json:"email"` jwt.StandardClaims } func main() { var hmacSampleSecret = []byte("111") // Das im vorherigen Beispiel generierte Token tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuWwj-aYjiIsImdlbmRlciI6IueUtyIsImF2YXRhciI6Imh0dHBzOi8vMS5qcGciLCJlbWFpbCI6InRlc3RAMTYzLmNvbSJ9.mJlWv5lblREwgnP6wWg-P75VC1FqQTs8iOdOzX6Efqk" token, err := jwt.ParseWithClaims(tokenString, &CustomerClaims{}, func(t *jwt.Token) (interface{}, error) { return hmacSampleSecret, nil }) if err!= nil { fmt.Println(err) return } claims := token.Claims.(*CustomerClaims) fmt.Println(claims) }
Verwenden von JWT in Gin-Projekten
Im Gin-Framework wird die Login-Authentifizierung im Allgemeinen durch Middleware implementiert. Die Bibliothek github.com/appleboy/gin-jwt
hat die Implementierung von github.com/golang-jwt/jwt
integriert und entsprechende Middleware und Controller für uns definiert. Das Folgende ist ein konkretes Beispiel:
package main import ( "log" "net/http" "time" jwt "github.com/appleboy/gin-jwt/v2" "github.com/gin-gonic/gin" ) // Wird verwendet, um den Benutzernamen und das Passwort für die Anmeldung zu empfangen type login struct { Username string `form:"username" json:"username" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } var identityKey = "id" // Daten im Payload von jwt type User struct { UserName string FirstName string LastName string } func main() { // Definieren einer Gin-Middleware authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ Realm: "test zone", // Identifizierung SigningAlgorithm: "HS256", // Verschlüsselungsalgorithmus Key: []byte("secret key"), // Geheimer Schlüssel Timeout: time.Hour, MaxRefresh: time.Hour, // Maximale Aktualisierungsverlängerungszeit IdentityKey: identityKey, // Geben Sie die ID des Cookies an PayloadFunc: func(data interface{}) jwt.MapClaims { // Payload, wo die Daten im Payload des zurückgegebenen jwt definiert werden können if v, ok := data.(*User); ok { return jwt.MapClaims{ identityKey: v.UserName, } } return jwt.MapClaims{} }, IdentityHandler: func(c *gin.Context) interface{} { claims := jwt.ExtractClaims(c) return &User{ UserName: claims[identityKey].(string), } }, Authenticator: Authenticator, // Die Login-Verifizierungslogik kann hier geschrieben werden Authorizator: func(data interface{}, c *gin.Context) bool { // Wenn ein Benutzer eine eingeschränkte Oberfläche über ein Token anfordert, wird diese Logik ausgeführt if v, ok := data.(*User); ok && v.UserName == "admin" { return true } return false }, Unauthorized: func(c *gin.Context, code int, message string) { // Antwort, wenn ein Fehler vorliegt c.JSON(code, gin.H{ "code": code, "message": message, }) }, // Geben Sie an, wo das Token abgerufen werden soll. Das Format ist: "<source>:<name>". Wenn es mehrere gibt, trennen Sie sie durch Kommas TokenLookup: "header: Authorization, query: token, cookie: jwt", TokenHeadName: "Bearer", TimeFunc: time.Now, }) if err!= nil { log.Fatal("JWT Error:" + err.Error()) } r := gin.Default() // Login-Schnittstelle r.POST("/login", authMiddleware.LoginHandler) auth := r.Group("/auth") // Logout auth.POST("/logout", authMiddleware.LogoutHandler) // Token aktualisieren, die Gültigkeitsdauer des Tokens verlängern auth.POST("/refresh_token", authMiddleware.RefreshHandler) auth.Use(authMiddleware.MiddlewareFunc()) // Anwenden der Middleware { auth.GET("/hello", helloHandler) } if err := http.ListenAndServe(":8005", r); err!= nil { log.Fatal(err) } } func Authenticator(c *gin.Context) (interface{}, error) { var loginVals login if err := c.ShouldBind(&loginVals); err!= nil { return "", jwt.ErrMissingLoginValues } userID := loginVals.Username password := loginVals.Password if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") { return &User{ UserName: userID, LastName: "Leapcell", FirstName: "Admin", }, nil } return nil, jwt.ErrFailedAuthentication } // Controller zur Behandlung der /hello-Route func helloHandler(c *gin.Context) { claims := jwt.ExtractClaims(c) user, _ := c.Get(identityKey) c.JSON(200, gin.H{ "userID": claims[identityKey], "userName": user.(*User).UserName, "text": "Hello World.", }) }
Nach dem Ausführen des Servers senden Sie eine Login-Anfrage über den Curl-Befehl, z. B.:
curl http://localhost:8005/login -d "username=admin&password=admin"
Das Antwort-Ergebnis gibt das Token zurück, z. B.:
{"code":200,"expire":"2021-12-16T17:33:39+08:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI"}
Leapcell: Die beste Serverless-Plattform für Golang-Hosting
Abschließend möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Golang-Diensten eignet: Leapcell
1. Multi - Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- Pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay - as - you - go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real - time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto - scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ