https://a.storyblok.com/f/270183/1368x665/70564f4aa7/25oct_dev-blog_jwt-go.jpg

Verwendung von JWT für die Authentifizierung in einer Golang-Anwendung

Zuletzt aktualisiert am October 14, 2025

Lesedauer: 14 Minuten

Dieser Artikel wurde im März 2025 aktualisiert.

Ein JSON-Web-Token (JWT) ist eine kompakte und in sich geschlossene Methode zur sicheren Übertragung von Informationen zwischen Parteien in Form eines JSON-Objekts und wird von Entwicklern häufig in ihren APIs verwendet.

JWTs sind beliebt, weil:

  1. Ein JWT ist zustandslos. Das heißt, es muss im Gegensatz zu undurchsichtigen Token nicht in einer Datenbank (Persistenzschicht) gespeichert werden.

  2. Die Signatur eines JWT wird niemals entschlüsselt, wenn sie einmal gebildet wurde, wodurch die Sicherheit des Tokens gewährleistet ist.

  3. Ein JWT kann so eingestellt werden, dass es nach einer bestimmten Zeitspanne ungültig wird. Dies trägt dazu bei, den Schaden, den ein Hacker anrichten kann, zu minimieren oder ganz zu beseitigen, falls das Token entwendet wird.

In diesem Tutorial werde ich die Erstellung, Verwendung und Ungültigmachung eines JWT mit einer einfachen RESTful-API unter Verwendung von Golang und der Vonage Messages-API demonstrieren.

Vonage API-Konto

Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.

In diesem Lernprogramm wird auch eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Rufnummern > Rufnummern kaufen und suchen Sie nach einer Nummer, die Ihren Anforderungen entspricht.

Was macht ein JWT aus?

Ein JWT setzt sich aus drei Teilen zusammen:

  • Header: die Art des Tokens und der verwendete Signieralgorithmus. Der Token-Typ kann "JWT" sein, während der Signieralgorithmus entweder HMAC oder SHA256 sein kann.

  • Nutzdaten: der zweite Teil des Tokens, der die Ansprüche enthält. Zu diesen Angaben gehören anwendungsspezifische Daten (z. B. Benutzerkennung, Benutzername), Gültigkeitsdauer des Tokens (exp), Aussteller (iss), Betreff (sub) und so weiter.

  • Signatur: Die verschlüsselte Kopfzeile, die verschlüsselte Nutzlast und ein von Ihnen angegebenes Geheimnis werden zur Erstellung der Signatur verwendet.

Verwenden wir ein einfaches Token, um die obigen Konzepte zu verstehen.

Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtM

Keine Sorge, das Token ist ungültig und kann daher in keiner Produktionsanwendung verwendet werden.

Sie können navigieren zu jwt.to und die Token-Signatur testen, ob sie verifiziert ist oder nicht. Verwenden Sie "HS512" als Algorithmus. Sie erhalten die Meldung "Signatur verifiziert":

A JSON Web Token broken down using JWT.ioA JSON Web Token broken down using JWT.io

Um die Signatur zu erstellen, muss Ihre Anwendung einen Schlüssel. Dieser Schlüssel sorgt dafür, dass die Signatur sicher bleibt - selbst wenn das JWT entschlüsselt wird, bleibt die Signatur verschlüsselt. Es wird dringend empfohlen, bei der Erstellung eines JWT immer ein Geheimnis zu verwenden.

Token-Typen

Da ein JWT so eingestellt werden kann, dass es nach einer bestimmten Zeitspanne abläuft (ungültig wird), werden in dieser Anwendung zwei Token berücksichtigt:

  • Zugriffstoken: Ein Zugriffstoken wird für Anfragen verwendet, die eine Authentifizierung erfordern. Es wird normalerweise in die Kopfzeile der Anfrage eingefügt. Es wird empfohlen, dass ein Zugriffstoken eine kurze Lebensdauer hat, etwa 15 Minuten. Eine kurze Zeitspanne für ein Zugriffstoken kann schwerwiegende Schäden verhindern, wenn das Token eines Benutzers manipuliert wird, falls das Token entwendet wird. Der Hacker hat nur 15 Minuten oder weniger Zeit, um seine Operationen auszuführen, bevor das Token ungültig gemacht wird.

  • Auffrischungs-Token: Ein Aktualisierungs-Token hat eine längere Lebensdauer, in der Regel 7 Tage. Dieses Token wird verwendet, um neue Zugangs- und Aktualisierungs-Tokens zu erzeugen. Wenn das Zugriffstoken abläuft, werden neue Sätze von Zugriffs- und Aktualisierungs-Tokens erstellt, wenn die Aktualisierungs-Token-Route aufgerufen wird (von unserer Anwendung aus).

Wo wird ein JWT gespeichert?

Für eine produktionsreife Anwendung wird dringend empfohlen, JWTs in einem HttpOnly Cookie zu speichern. Um dies zu erreichen, wird beim Senden des vom Backend generierten Cookies an das Frontend (Client) ein HttpOnly Flag zusammen mit dem Cookie gesendet, um den Browser anzuweisen, das Cookie nicht durch die clientseitigen Skripte anzuzeigen. Auf diese Weise können XSS-Angriffe (Cross Site Scripting) verhindert werden. JWT können auch im lokalen Speicher des Browsers oder im Sitzungsspeicher gespeichert werden. Die Speicherung eines JWT auf diese Weise kann verschiedene Angriffe wie die oben erwähnten XSS-Angriffe nach sich ziehen und ist daher im Allgemeinen weniger sicher als die Verwendung der HttpOnly Cookie-Technik.

Die Anwendung

Wir betrachten eine einfache todo restful API. Bevor wir beginnen, wird vorausgesetzt, dass Sie einige der Grundlagen von Go kennen.

Erstellen Sie ein Verzeichnis namens jwt-todound initialisieren Sie dann go.mod für die Verwaltung von Abhängigkeiten. go.mod wird mit initialisiert:

go mod init jwt-todo

Erstellen Sie nun eine main.go Datei im Stammverzeichnis (/jwt-todo) als Einstiegspunkt in Ihre Anwendung und fügen Sie diese Datei hinzu:

package main

func main() {

}

Wir verwenden gin für das Routing und die Bearbeitung von HTTP-Anfragen. Das Gin-Framework hilft bei der Reduzierung von "Boilerplate"-Code und ist sehr effizient beim Aufbau skalierbarer APIs.

Sie können gin installieren, wenn Sie es noch nicht getan haben, indem Sie:

go get github.com/gin-gonic/gin

Aktualisieren Sie dann die main.go Datei:

package main

import (
 "github.com/gin-gonic/gin"
)

router := gin.Default()

func main() {
  router.POST("/login", Login)
  log.Fatal(router.Run(":8080"))
}

Wir beginnen also mit dem Import von gin und erstellen dann eine router. Wir werden eine POST-Anfrage senden, um uns anzumelden, und Sie werden feststellen, dass die Login Funktion, die noch nicht definiert wurde (dazu kommen wir noch).

In einer idealen Situation nimmt die /login Route die Anmeldedaten eines Benutzers, prüft sie mit einer Datenbank und meldet ihn an, wenn die Anmeldedaten gültig sind. In dieser API werden wir jedoch einen Dummy-Benutzer erstellen, der im Code definiert ist. Um einen Beispielbenutzer in einer Struktur zu erstellen, fügen Sie Folgendes in die main.go Datei hinzu:

type User struct {
 ID uint64       `json:"id"`
 Username string `json:"username"`
 Password string `json:"password"`
 Phone string `json:phone`
}

// a dummy user
var user = User{
 ID:          1,
 Username: "username",
 Password: "password",
}

Login-Anfrage

Ich erwähnte, dass wir zu der fehlenden Login() Funktion, die ausgeführt wird, wenn wir die eingehenden Benutzerdaten in der Anfrage mit den aus einer Datenquelle abgerufenen Daten vergleichen wollen. Mit Dependency Injection, *gin.Context zieht die Anfrage in eine Variable c die wir dann der User struct per Referenz an ShouldBindJSON() übergeben, um sicherzustellen, dass das JSON gültig ist. Der Code sieht wie folgt aus:

func Login(c *gin.Context) {
  var u User
  if err := c.ShouldBindJSON(&u); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
     return
  }

  // Compare the user from the request, with the one we defined:
  if user.Username != u.Username || user.Password != u.Password {
     c.JSON(http.StatusUnauthorized, "Please provide valid login details")
     return
  }

  token, err := CreateToken(user.ID)
  if err != nil {
     c.JSON(http.StatusUnprocessableEntity, err.Error())
     return
  }
  c.JSON(http.StatusOK, token)
}

Wir haben die Anfrage des Benutzers erhalten und sie in die User Struktur. Dann haben wir den eingegebenen Benutzer mit dem im Speicher definierten Benutzer verglichen. Hätten wir eine Datenbank verwendet, hätten wir ihn mit einem Datensatz in der Datenbank verglichen.

Um die Funktion nicht zu Login Funktion nicht aufzublähen, wird die Logik zur Erzeugung eines JWT von CreateToken. Beachten Sie, dass die Benutzerkennung an diese Funktion übergeben wird. Sie wird als Anspruch verwendet, wenn das JWT erzeugt wird.

Die Funktion CreateToken Funktion macht Gebrauch vom dgrijalva/jwt-go Paket, das wir mit installieren können:

go get github.com/dgrijalva/jwt-go

Definieren wir die CreateToken Funktion:

func CreateToken(userid uint64) (string, error) {
  var err error
  //Creating Access Token
  os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
  atClaims := jwt.MapClaims{}
  atClaims["authorized"] = true
  atClaims["user_id"] = userid
  atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
  at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
  token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
  if err != nil {
     return "", err
  }
  return token, nil
}

Wir haben das Token so eingestellt, dass es nur 15 Minuten lang gültig ist. Danach ist es ungültig und kann nicht mehr für eine authentifizierte Anfrage verwendet werden. Beachten Sie auch, dass wir das JWT mit einem geheimen(ACCESS_SECRET) signiert haben, das wir aus unserer Umgebungsvariablen erhalten haben. Sie sollten Geheimnisse immer in einer Umgebungsvariablendatei exklusiv für Ihren lokalen, Produktions- oder Test-Stack speichern und sicherstellen, dass Sie Geheimnisse niemals in Ihren Code übertragen.

Bislang sieht unsere main.go Datei wie folgt aus:

package main

import (
	"log"
	"net/http"
	"os"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
)

type User struct {
	ID       uint64 `json:"id"`
	Username string `json:"username"`
	Password string `json:"password"`
	Phone    string `json:"phone"`
}

var dummyUser = User{
	ID:       1,
	Username: "username",
	Password: "password",
	Phone:    "49123454322",
}

func CreateToken(userid uint64) (string, error) {
	var err error

	os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
	atClaims := jwt.MapClaims{}
	atClaims["authorized"] = true
	atClaims["user_id"] = userid
	atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
	at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
	token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))

	if err != nil {
		return "", err
	}

	return token, nil
}

func Login(c *gin.Context) {
	var u User
	if err := c.ShouldBindJSON(&u); err != nil {
		c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
		return
	}
	// Compare the user from the request, with the one we defined:
	if dummyUser.Username != u.Username || dummyUser.Password != u.Password {
		c.JSON(http.StatusUnauthorized, "Please provide valid login details")
		return
	}
	token, err := CreateToken(dummyUser.ID)
	if err != nil {
		c.JSON(http.StatusUnprocessableEntity, err.Error())
		return
	}
	c.JSON(http.StatusOK, token)
}

func main() {
	router := gin.Default()
	router.POST("/login", Login)
	log.Fatal(router.Run(":8080"))
}

Jetzt können wir die Anwendung ausführen:

go run main.go

Jetzt können wir es ausprobieren und sehen, was wir bekommen! Starten Sie Ihr bevorzugtes API-Tool und wählen Sie den login Endpunkt:

Making a request using PostmanMaking a request using Postman

Wie oben zu sehen, haben wir einen JWT generiert, der 15 Minuten lang gültig ist.

Schlupflöcher bei der Umsetzung

Ja, wir können einen Benutzer anmelden und ein JWT generieren, aber bei der obigen Implementierung ist vieles falsch:

  1. Das JWT kann nur ungültig gemacht werden, wenn es abläuft. Eine wesentliche Einschränkung besteht darin, dass ein Benutzer sich anmelden und dann sofort wieder abmelden kann, aber das JWT des Benutzers bleibt gültig, bis die Ablaufzeit erreicht ist.

  2. Das JWT kann von einem Hacker entwendet und verwendet werden, ohne dass der Benutzer etwas dagegen unternimmt, bis das Token abläuft.

  3. Der Benutzer muss sich nach Ablauf des Tokens erneut anmelden, was zu einer schlechten Benutzererfahrung führt.

Wir können die oben genannten Probleme auf zwei Arten angehen:

  1. Verwendung einer Persistenzspeicherschicht zur Speicherung von JWT-Metadaten. Dadurch können wir ein JWT in der Sekunde ungültig machen, in der sich der Benutzer abmeldet, und so die Sicherheit verbessern.

  2. Unter Verwendung des Konzepts eines Aktualisierungs-Token zur Erzeugung eines neuen Zugangs-Tokenfür den Fall, dass das Zugangstoken abgelaufen ist, und verbessert so die Benutzerfreundlichkeit.

Redis zum Speichern von JWT-Metadaten verwenden

Eine der Lösungen, die wir oben angeboten haben, ist die Speicherung von JWT-Metadaten in einer Persistenzschicht. Dies kann in jeder beliebigen Persistenzschicht erfolgen, aber Redis ist sehr empfehlenswert. Da die von uns generierten JWTs eine Verfallszeit haben, verfügt Redis über eine Funktion, die Daten, deren Verfallszeit erreicht ist, automatisch löscht. Redis kann auch viele Schreibvorgänge verarbeiten und ist horizontal skalierbar.

Da Redis ein Key-Value-Speicher ist, müssen die Schlüssel eindeutig sein. Um dies zu erreichen, verwenden wir uuid als Schlüssel und die Benutzer-ID als Wert.

Installieren wir also zwei Pakete zur Verwendung:

go get github.com/go-redis/redis/v7
go get github.com/google/uuid

Wir werden diese auch in die main.go Datei wie folgt:

import (

  "github.com/go-redis/redis/v7"
  "github.com/google/uuid"

)

Hinweis: Es wird erwartet, dass Sie redis auf Ihrem lokalen Rechner installiert haben. Wenn dies nicht der Fall ist, können Sie die Installation unterbrechen, bevor Sie fortfahren.

Lassen Sie uns nun Redis initialisieren:

var redisClient *redis.Client

func init() {
  // Initializing Redis
  dsn := os.Getenv("REDIS_DSN")
  if len(dsn) == 0 {
     dsn = "localhost:6379"
  }

  redisClient = redis.NewClient(&redis.Options{
     Addr: dsn, // Redis Port
  })

  _, err := redisClient.Ping().Result()
  if err != nil {
     panic(err)
  }
}

Der Redis-Client wird in der Methode magic initialisiert init() Methode initialisiert. Dadurch wird sichergestellt, dass jedes Mal, wenn wir die main.go Datei aufrufen, wird Redis automatisch verbunden.

Wenn wir von nun an ein Token erstellen, erzeugen wir ein uuid generiert, das als einer der Token-Ansprüche verwendet wird, so wie wir in der vorherigen Implementierung die Benutzerkennung als Anspruch verwendet haben.

Definieren Sie die Metadaten

In unserer vorgeschlagenen Lösung müssen wir statt eines Tokens zwei JWTs erstellen:

  1. Der Zugangstoken

  2. Das Refresh-Token

Um dies zu erreichen, müssen wir eine Struktur definieren, die diese Token-Definitionen, ihre Verfallszeiten und Uuids enthält:

type TokenDetails struct {
  AccessToken  string
  RefreshToken string
  AccessUuid   string
  RefreshUuid  string
  AtExpires    int64
  RtExpires    int64
}

Die Verfallszeit und die uuids sind sehr praktisch, da sie beim Speichern von Token-Metadaten in Redis verwendet werden.

Aktualisieren wir nun die CreateToken Funktion so, dass sie wie folgt aussieht:

func CreateToken(userid uint64) (*TokenDetails, error) {
	td := &TokenDetails{}
	td.AtExpires = time.Now().Add(time.Minute * 15).Unix()
	td.AccessUuid = uuid.New().String()

	td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
	td.RefreshUuid = uuid.New().String()

	var err error

	// Creating Access Token
	os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
	atClaims := jwt.MapClaims{}
	atClaims["authorized"] = true
	atClaims["access_uuid"] = td.AccessUuid
	atClaims["user_id"] = userid
	atClaims["exp"] = td.AtExpires
	at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
	td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))

	if err != nil {
		return nil, err
	}

	// Creating Refresh Token
	os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file
	rtClaims := jwt.MapClaims{}
	rtClaims["refresh_uuid"] = td.RefreshUuid
	rtClaims["user_id"] = userid
	rtClaims["exp"] = td.RtExpires
	rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
	td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET")))

	if err != nil {
		return nil, err
	}

	return td, nil
}

In der obigen Funktion wird das Zugangs-Token nach 15 Minuten ab und das Auffrischungs-Token läuft nach 7 Tagen ab. Sie können auch sehen, dass wir jedem Token eine uuid als Anspruch hinzugefügt haben.

Da die uuid jedes Mal, wenn sie erstellt wird, eindeutig ist, kann ein Benutzer mehr als ein Token erstellen. Dies geschieht, wenn ein Benutzer auf verschiedenen Geräten angemeldet ist. Der Benutzer kann sich auch von einem der Geräte abmelden, ohne von allen Geräten abgemeldet zu werden. Wie cool!

Speichern von JWTs-Metadaten

Lassen Sie uns nun die Funktion verdrahten, die zum Speichern der JWT-Metadaten verwendet wird:

func CreateAuth(userid uint64, td *TokenDetails) error {
	at := time.Unix(td.AtExpires, 0)
	rt := time.Unix(td.RtExpires, 0)
	now := time.Now()

	errAccess := redisClient.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err()

	if errAccess != nil {
		return errAccess
	}

	errRefresh := redisClient.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err()

	if errRefresh != nil {
		return errRefresh
	}

	return nil
}

Stellen Sie sicher, dass Sie den Import für die Standardbibliothek strcov Funktion am Anfang der main.go Datei hinzufügen.

Wir haben die TokenDetails die Informationen über die Ablaufzeit der JWTs und die bei der Erstellung der JWTs verwendeten uuids enthalten. Wenn die Verfallszeit erreicht ist, entweder für das Aktualisierungs-Token oder das Zugriffs-Tokenerreicht, wird das JWT automatisch aus Redis gelöscht.

Ich persönlich benutze Redilyeine Redis-GUI. Das ist ein nettes Tool. Sie können sich unten ansehen, wie JWT-Metadaten in einem Schlüssel-Wert-Paar gespeichert werden.

Using Readily to see the stored metadata in RedisUsing Readily to see the stored metadata in RedisBevor wir die Anmeldung erneut testen, müssen wir die CreateAuth() Funktion in der Login() Funktion aufrufen. Aktualisieren Sie die Login-Funktion wie folgt:

func Login(c *gin.Context) {
	var u User
	
	if err := c.ShouldBindJSON(&u); err != nil {
		c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
		return
	}

	// Compare the user from the request, with the one we defined:
	if dummyUser.Username != u.Username || dummyUser.Password != u.Password {
		c.JSON(http.StatusUnauthorized, "Please provide valid login details")
		return
	}

	ts, err := CreateToken(dummyUser.ID)

	if err != nil {
		c.JSON(http.StatusUnprocessableEntity, err.Error())
		return
	}

	saveErr := CreateAuth(dummyUser.ID, ts)

	if saveErr != nil {
		c.JSON(http.StatusUnprocessableEntity, saveErr.Error())
	}

	tokens := map[string]string{
		"access_token":  ts.AccessToken,
		"refresh_token": ts.RefreshToken,
	}

	c.JSON(http.StatusOK, tokens)
}

Wir können versuchen, uns erneut anzumelden. Speichern Sie die main.go Datei und führen Sie sie aus. Wenn die Anmeldung von Postman getroffen wird, sollten wir haben:

Checking the access and refresh token reponse in PostmanChecking the access and refresh token reponse in PostmanAusgezeichnet! Wir haben sowohl den zugangs_token und den refresh_tokenund haben auch die Token-Metadaten in Redis persistiert.

Todo erstellen

Jetzt können wir Anfragen stellen, die eine Authentifizierung mit JWT erfordern.

Eine der unauthentifizierten Anfragen in dieser API ist die Erstellung von todo Anfrage.

Definieren wir zunächst eine Todo Struktur:

type Todo struct {
  UserID uint64 `json:"user_id"`
  Title string `json:"title"`
}

Bei der Durchführung einer authentifizierten Anfrage müssen wir das im Authentifizierungs-Header übergebene Token validieren, um zu sehen, ob es gültig ist. Wir müssen einige Hilfsfunktionen definieren, die dabei helfen.

Zunächst müssen wir das Token aus dem Request-Header extrahieren, indem wir die ExtractToken Funktion:

func ExtractToken(r *http.Request) string {
	bearToken := r.Header.Get("Authorization")
	// Normally Authorization the_token_xxx
	strArr := strings.Split(bearToken, " ")

	if len(strArr) == 2 {
		return strArr[1]
	}

	return ""
}

Anschließend schreiben wir die Funktion zur Überprüfung des Tokens:

func VerifyToken(r *http.Request) (*jwt.Token, error) {
	tokenString := ExtractToken(r)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Make sure that the token method conform to "SigningMethodHMAC"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("ACCESS_SECRET")), nil
	})

	if err != nil {
		return nil, err
	}

	return token, nil
}

Stellen Sie sicher, dass Sie den Import für die Standardbibliothek hinzufügen fmt Bibliothek. Wir haben ExtractToken innerhalb der VerifyToken Funktion auf, um die Token-Zeichenkette zu erhalten, und überprüfen dann die Signiermethode.

Dann überprüfen wir die Gültigkeit dieses Tokens, ob es noch brauchbar ist oder abgelaufen ist, indem wir die TokenValid Funktion:

func TokenValid(r *http.Request) error {
	token, err := VerifyToken(r)
	if err != nil {
		return err
	}

	if !token.Valid {
		return err
	}

	return nil
}

Wir werden auch das Token extrahieren Metadaten die in unserem Redis-Speicher, den wir zuvor eingerichtet haben, nachschlagen werden. Um das Token zu extrahieren, definieren wir die ExtractTokenMetadata Funktion:

func ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
	token, err := VerifyToken(r)

	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(jwt.MapClaims)

	if ok && token.Valid {
		accessUuid, ok := claims["access_uuid"].(string)

		if !ok {
			return nil, err
		}

		userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)

		if err != nil {
			return nil, err
		}

		return &AccessDetails{
			AccessUuid: accessUuid,
			UserId:     userId,
		}, nil
	}

	return nil, err
}

Die Funktion ExtractTokenMetadata Funktion gibt ein AccessDetails (die eine Struktur ist) zurück. Diese Struktur enthält die Metadaten (access_uuid und user_id), die wir benötigen, um eine Suche in Redis durchzuführen. Wenn wir die Metadaten aus irgendeinem Grund nicht von diesem Token erhalten können, wird die Anfrage mit einer Fehlermeldung abgebrochen.

Die oben erwähnte AccessDetails Struktur sieht folgendermaßen aus:

type AccessDetails struct {
    AccessUuid string
    UserId   uint64
}

Wir haben auch erwähnt, dass wir die Token-Metadaten in Redis nachschlagen können. Definieren wir eine Funktion, mit der wir das tun können:

func FetchAuth(authD *AccessDetails) (uint64, error) {
	userid, err := redisClient.Get(authD.AccessUuid).Result()

	if err != nil {
		return 0, err
	}

	userID, _ := strconv.ParseUint(userid, 10, 64)

	return userID, nil
}

FetchAuth() akzeptiert die AccessDetails von der ExtractTokenMetadata Funktion und sucht dann in Redis nach. Wenn der Datensatz nicht gefunden wird, kann dies bedeuten, dass das Token abgelaufen ist und ein Fehler ausgelöst wird.

Lassen Sie uns schließlich die CreateTodo Funktion, um die Implementierung der obigen Funktionen besser zu verstehen:

func CreateTodo(c *gin.Context) {
	var td *Todo

	if err := c.ShouldBindJSON(&td); err != nil {
		c.JSON(http.StatusUnprocessableEntity, "invalid json")
		return
	}

	tokenAuth, err := ExtractTokenMetadata(c.Request)

	if err != nil {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	userId, err := FetchAuth(tokenAuth)

	if err != nil {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	td.UserID = userId

	// You can proceed to save the Todo to a database
	// but we will just return it to the caller here:
	c.JSON(http.StatusCreated, td)
}

Wie gesehen, haben wir die ExtractTokenMetadata zum Extrahieren der JWT Metadaten die in FetchAuth verwendet wird, um zu prüfen, ob die Metadaten noch in unserem Redis-Speicher vorhanden sind. Wenn alles in Ordnung ist, kann das Todo in der Datenbank gespeichert werden, aber wir haben uns entschieden, es an den Aufrufer zurückzugeben.

Aktualisieren wir main() um die Funktion CreateTodo Funktion:

func main() {
	router := gin.Default()
	router.POST("/login", Login)
	router.POST("/todo", CreateTodo)
	log.Fatal(router.Run(":8080"))
}

Zum Testen CreateTodozu testen, loggen Sie sich ein und kopieren Sie die access_token und fügen es dem Autorisierungs-Träger-Token Feld wie folgt ein:

Testing the tokens using PostmanTesting the tokens using Postman

Fügen Sie dann dem Anfragetext einen Titel hinzu, um eine Aufgabe zu erstellen, und stellen Sie eine POST-Anfrage an den /todo Endpunkt, wie unten gezeigt:

Checking the response using PostmanChecking the response using Postman

Der Versuch, ein Todo ohne ein access_token zu erstellen, wird nicht autorisiert sein:

Checking an unauthorised request in PostmanChecking an unauthorised request in Postman

Logout-Anfrage

Bislang haben wir gesehen, wie ein JWT verwendet wird, um eine authentifizierte Anfrage zu stellen. Wenn sich ein Benutzer abmeldet, werden wir sein JWT sofort widerrufen/ungültig machen. Dies wird durch das Löschen der JWT-Metadaten aus unserem Redis-Speicher erreicht.

Wir werden nun eine Funktion definieren, mit der wir JWT-Metadaten aus Redis löschen können:

func DeleteAuth(givenUuid string) (int64, error) {
	deleted, err := redisClient.Del(givenUuid).Result()

	if err != nil {
		return 0, err
	}

	return deleted, nil
}

Die obige Funktion löscht den Datensatz in redis, der dem als Parameter übergebenen uuid als Parameter übergeben wurde.

Die Funktion Logout Funktion sieht folgendermaßen aus:

func Logout(c *gin.Context) {
	au, err := ExtractTokenMetadata(c.Request)

	if err != nil {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	deleted, delErr := DeleteAuth(au.AccessUuid)

	if delErr != nil || deleted == 0 {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	c.JSON(http.StatusOK, "Successfully logged out")
}

In der Logout Funktion werden zunächst die JWT-Metadaten extrahiert. Bei Erfolg werden diese Metadaten dann gelöscht, wodurch das JWT sofort ungültig wird.

Aktualisieren Sie vor dem Testen die main.go Datei, um den logout Endpunkt wie folgt:

func main() {
	router := gin.Default()
	router.POST("/login", Login)
	router.POST("/todo", CreateTodo)
	router.POST("/logout", Logout)
	log.Fatal(router.Run(":8080"))
}

Geben Sie einen gültigen access_token mit einem Benutzer verknüpft, dann melden Sie den Benutzer ab. Denken Sie daran, das access_token zum Authorization Bearer Tokenhinzufügen und dann den Logout-Endpunkt anklicken:

Log out request using PostmanLog out request using Postman

Jetzt ist der Benutzer abgemeldet, und es kann keine weitere Anfrage mit diesem JWT mehr durchgeführt werden, da es sofort ungültig gemacht wird. Diese Implementierung ist sicherer als das Warten darauf, dass ein JWT nach der Abmeldung eines Benutzers abläuft.

Absicherung authentifizierter Routen

Wir haben zwei Routen, die eine Authentifizierung erfordern: /login und /logout. Im Moment kann jeder, mit oder ohne Authentifizierung, auf diese Routen zugreifen. Lassen Sie uns das ändern.

Wir müssen die Funktion TokenAuthMiddleware() Funktion definieren, um diese Routen zu sichern:

func TokenAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		err := TokenValid(c.Request)

		if err != nil {
			c.JSON(http.StatusUnauthorized, err.Error())
			c.Abort()
			return
		}

		c.Next()
	}
}

Wie oben gesehen, haben wir die Funktion TokenValid() Funktion (die zuvor definiert wurde) aufgerufen, um zu prüfen, ob das Token noch gültig ist oder abgelaufen ist. Die Funktion wird in den authentifizierten Routen verwendet, um sie zu sichern. Aktualisieren wir nun main.go um diese Middleware einzubinden:

func main() {
	router := gin.Default()
	router.POST("/login", Login)
	router.POST("/todo", TokenAuthMiddleware(), CreateTodo)
	router.POST("/logout", TokenAuthMiddleware(), Logout)
	log.Fatal(router.Run(":8080"))
}

Erfrischende Token

Bislang können wir JWTs erstellen, verwenden und widerrufen. Was passiert in einer Anwendung, die eine Benutzeroberfläche beinhaltet, wenn das Zugriffstoken abläuft und der Benutzer eine authentifizierte Anfrage stellen muss? Ist der Benutzer dann nicht autorisiert und muss sich erneut anmelden? Leider wird dies der Fall sein. Dies kann jedoch durch das Konzept eines Aktualisierungs-Token. Der Benutzer braucht sich nicht erneut anzumelden. Das Aktualisierungs-Token wird zusammen mit dem Zugangs-Token erstellt wurde, wird verwendet, um neue Paare von Zugangs- und Aktualisierungs-Token.

Wenn wir JavaScript für unsere API-Endpunkte verwenden, können wir die JWTs im Handumdrehen aktualisieren, indem wir axios Abfangjäger. In unserer API müssen wir eine POST-Anfrage mit einer refresh_token als Body an den /token/refresh Endpunkt senden.

Erstellen wir zunächst die Refresh() Funktion:

func Refresh(c *gin.Context) {
	mapToken := map[string]string{}

	if err := c.ShouldBindJSON(&mapToken); err != nil {
		c.JSON(http.StatusUnprocessableEntity, err.Error())
		return
	}

	refreshToken := mapToken["refresh_token"]

	// Verify the token
	os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf")
	token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
		// Make sure that the token method conform to "SigningMethodHMAC"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}

		return []byte(os.Getenv("REFRESH_SECRET")), nil
	})

	// If there is an error, the token must have expired
	if err != nil {
		c.JSON(http.StatusUnauthorized, "Refresh token expired")
		return
	}

	// Is token valid?
	if !token.Valid {
		c.JSON(http.StatusUnauthorized, err)
		return
	}

	// Since token is valid, get the uuid:
	claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims

	if ok && token.Valid {
		refreshUuid, ok := claims["refresh_uuid"].(string) // Convert the interface to string

		if !ok {
			c.JSON(http.StatusUnprocessableEntity, err)
			return
		}

		userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)

		if err != nil {
			c.JSON(http.StatusUnprocessableEntity, "Error occurred")
			return
		}

		// Delete the previous Refresh Token
		deleted, delErr := DeleteAuth(refreshUuid)

		if delErr != nil || deleted == 0 { //if any goes wrong
			c.JSON(http.StatusUnauthorized, "unauthorized")
			return
		}

		// Create new pairs of refresh and access tokens
		ts, createErr := CreateToken(userId)
		if createErr != nil {
			c.JSON(http.StatusForbidden, createErr.Error())
			return
		}

		// Save the tokens metadata to redis
		saveErr := CreateAuth(userId, ts)
		if saveErr != nil {
			c.JSON(http.StatusForbidden, saveErr.Error())
			return
		}

		tokens := map[string]string{
			"access_token":  ts.AccessToken,
			"refresh_token": ts.RefreshToken,
		}
		c.JSON(http.StatusCreated, tokens)
	} else {
		c.JSON(http.StatusUnauthorized, "refresh expired")
	}
}

Auch wenn in dieser Funktion eine Menge passiert, sollten wir versuchen, den Ablauf zu verstehen.

  • Wir haben zunächst die refresh_token aus dem Anfragetext.

  • Anschließend haben wir die Signiermethode des Tokens überprüft.

  • Prüfen Sie dann, ob das Token noch gültig ist.

  • Die refresh_uuid und die user_id werden dann extrahiert, wobei es sich um Metadaten handelt, die bei der Erstellung des Aktualisierungs-Tokens als Ansprüche verwendet werden.

  • Wir suchen dann nach den Metadaten im redis-Speicher und löschen sie mit dem refresh_uuid als Schlüssel.

  • Anschließend erstellen wir ein neues Paar von Zugriffs- und Aktualisierungs-Tokens, die nun für zukünftige Anfragen verwendet werden.

  • Die Metadaten der Zugriffs- und Aktualisierungs-Tokens werden in redis gespeichert.

  • Die erstellten Token werden an den Aufrufer zurückgegeben.

  • In der else-Anweisung, wenn das Aktualisierungs-Token nicht gültig ist, kann der Benutzer kein neues Tokenpaar erstellen. Wir müssen uns dann erneut anmelden, um neue Token zu erhalten.

Als nächstes fügen Sie die Refresh-Token-Route in der main() Funktion hinzu:

router.POST("/token/refresh", Refresh)

Testen des Endpunkts mit einer gültigen refresh_token:

Testing the endpoint with a valid refresh token in PostmanTesting the endpoint with a valid refresh token in Postman

Und wir haben erfolgreich neue Tokenpaare erstellt. Großartig😎.

Senden von Nachrichten mit der Vonage Messages API

Lassen Sie uns die Benutzer jedes Mal benachrichtigen, wenn sie über die Vonage Messages-API ein Todo erstellen.

Sie können Ihren API-Schlüssel und Ihr Geheimnis in einer Umgebungsvariablen definieren und sie dann in dieser Datei wie folgt verwenden:

var (
  VONAGE_API_KEY   = os.Getenv( "your_api_key")
  VONAGE_API_SECRET  = os.Getenv("your_secret")
)

Dann werden wir einige Strukturen definieren, die Informationen über den Absender, den Empfänger und den Inhalt der Nachricht enthalten:

type Todo struct {
	UserID uint64 `json:"user_id"`
	Title  string `json:"title"`
}

type Payload struct {
	From    From    `json:"from"`
	To      To      `json:"to"`
	Message Message `json:"message"`
}

type From struct {
	Type   string `json:"type"`
	Number string `json:"number"`
}

type To struct {
	Type   string `json:"type"`
	Number string `json:"number"`
}

type Content struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type Message struct {
	Content Content `json:"content"`
}

Dann definieren wir die Funktion zum Senden einer Nachricht an einen Benutzer. Achten Sie darauf, dass Sie die encoding/json und bytes in Ihre Importe am Anfang Ihrer main.go .

func SendMessage(username, phone string) (*http.Response, error) {
	data := Payload{
		From: From{
			Type:   "sms",
			Number: "Vonage",
		},
		To: To{
			Type:   "sms",
			Number: phone,
		},
		Message: Message{
			Content: Content{
				Type: "text",
				Text: "Dear " + username + ", a todo was created from your account just now.",
			},
		},
	}

	payloadBytes, err := json.Marshal(data)

	if err != nil {
		return nil, err
	}

	body := bytes.NewReader(payloadBytes)

	req, err := http.NewRequest("POST", "https://api.nexmo.com/v0.1/messages", body)

	if err != nil {
		return nil, err
	}

	// Ensure headers
	req.SetBasicAuth(VONAGE_API_KEY, VONAGE_API_SECRET)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)

	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	return resp, nil
}

In der obigen Funktion ist die To Nummer die Nummer des Benutzers, während die From Nummer muss über Ihr Vonage API Dashboard.

Stellen Sie sicher, dass Sie Ihre VONAGE_API_KEY und VONAGE_API_SECRET in Ihrer Umgebungsvariablendatei definiert haben.

Wir aktualisieren dann die CreateTodo Funktion, um die soeben definierte SendMessage Funktion, die gerade definiert wurde, und übergeben die erforderlichen Parameter:

func CreateTodo(c *gin.Context) {
	var td *Todo

	if err := c.ShouldBindJSON(&td); err != nil {
		c.JSON(http.StatusUnprocessableEntity, "invalid json")
		return
	}

	tokenAuth, err := ExtractTokenMetadata(c.Request)

	if err != nil {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	userId, err := FetchAuth(tokenAuth)

	if err != nil {
		c.JSON(http.StatusUnauthorized, "unauthorized")
		return
	}

	td.UserID = userId

	msgResp, err := SendMessage(dummyUser.Username, dummyUser.Phone)
	
	if err != nil {
	   c.JSON(http.StatusForbidden, "error occurred sending message to user")
	   return
	}

	if msgResp.StatusCode > 299 {
	   c.JSON(http.StatusForbidden, "cannot send message to user")
	   return
	}

	// You can proceed to save the Todo to a database
	// but we will just return it to the caller here:
	c.JSON(http.StatusCreated, td)
}

Vergewissern Sie sich, dass eine gültige Telefonnummer angegeben ist, damit Sie die Nachricht erhalten können, wenn Sie versuchen, eine Aufgabe zu erstellen.

Schlussfolgerung

Sie haben gesehen, wie Sie ein JWT erstellen und ungültig machen können. Sie haben auch gesehen, wie Sie die Vonage Messages API in Ihre Golang-Anwendung integrieren können, um Benachrichtigungen zu senden. Weitere Informationen über Best Practices und die Verwendung eines JWT finden Sie in diesem GitHub-Repositorium.

Sie können diese Anwendung erweitern und eine echte Datenbank verwenden, um Benutzer und ToDo's zu speichern, und Sie können auch ein React oder VueJS verwenden, um ein Frontend zu erstellen. Hier werden Sie das Refresh Token Feature mit Hilfe von Axios Interceptors wirklich zu schätzen wissen.

Teilen Sie:

https://a.storyblok.com/f/270183/384x384/e4e7d1452e/benjamin-aronov.png
Benjamin AronovAdvokat für Entwickler

Benjamin Aronov ist ein Entwickler-Befürworter bei Vonage. Er ist ein bewährter Community Builder mit einem Hintergrund in Ruby on Rails. Benjamin genießt die Strände von Tel Aviv, das er sein Zuhause nennt. Von Tel Aviv aus kann er einige der besten Startup-Gründer der Welt treffen und von ihnen lernen. Außerhalb der Tech-Branche reist Benjamin gerne um die Welt auf der Suche nach dem perfekten Pain au Chocolat.