
Teilen Sie:
Victor Steven is a self-taught full-stack developer that loves researching about doing things differently. He has a degree in Engineering and over 5 years of experience as a software developer. Steven is very interested in designing and building scalable APIs and he enjoys writing about his discoveries in software development.
Verwendung von JWT für die Autentifizierung in einer Golang-Anwendung
Lesedauer: 14 Minuten
Einführung
Ein JSON Web Token (JWT) ist eine kompakte und unabhängige Methode zur sicheren Übertragung von Informationen zwischen den Parteien in Form eines JSON-Objekts und wird häufig von den Nutzern der APIs verwendet. Os JWTs são populares porque:
Um JWT é sem estado. Das heißt, er muss nicht in einer Datenbank (Dauerlaufkarte) gespeichert werden, im Gegensatz zu den Opak-Tokens.
Die Zusammensetzung eines JWT wird nach der Erstellung nicht mehr dekodiert, so dass der Token sicher und geschützt ist.
Ein JWT kann so konfiguriert werden, dass er nach einer bestimmten Zeitspanne ungültig wird. Dies hilft, jeglichen Schaden, der durch einen Hacker angerichtet werden könnte, zu minimieren oder ganz zu beseitigen, falls der Token gehackt wird.
In diesem Tutorial demonstrieren wir den Aufbau, die Verwendung und die Aufhebung eines JWT mit einer einfachen RESTful-API unter Verwendung von Golang und der Mensagens-API von Vonage.
Conta API Vonage
Um dieses Tutorial zu vervollständigen, benötigen Sie eine Vonage API. Wenn Sie noch keine haben, können Sie sich noch heute anmelden und mit Hilfe von Gratis-Krediten einrichten. Sobald Sie einen Vertrag haben, können Sie Ihren API-Schlüssel und Ihr API-Geheimnis im oberen Teil des API-Pakets von Vonage finden.
Auch in diesem Tutorial wird eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Números > Comprar Números und besorgen Sie sich eine Nummer, die Ihren Bedürfnissen entspricht. Wenn Sie sich angemeldet haben, können Sie die Erstanmeldung einer Nummer ganz einfach mit Ihrem verfügbaren Guthaben abrechnen.

Was macht ein JWT aus?
Um JWT é composto de três partes:
Header: die Art des Tokens und der verwendete Assinaturalgorithmus. Der Token-Typ kann "JWT" sein, und der Verschlüsselungsalgorithmus kann HMAC oder SHA256 sein.
Nutzlast: der zweite Teil des Tokens, der die Reaktivierungsdaten enthält. Zu diesen Reaktivierungsdaten gehören besondere Daten der Anwendung (z. B. Benutzeridentifikation, Benutzername), Ablaufdatum des Tokens (expiração), Absender, Aufgabe(n) und so weiter.
Signatur: Der verschlüsselte "Header", die verschlüsselte "Payload" und eine von Ihnen erstellte Senha werden zur Erstellung der Assinatur verwendet.
Vamos usar um token simples para entender os conceitos acima.
Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtMKeine Sorge, der Token ist unbrauchbar und funktioniert in keiner Produktionsanwendung.
Sie können auf der Website jwt.to besuchen und die Übereinstimmung des Tokens mit dem Algorithmus testen, ob dieser verifiziert ist oder nicht. Verwenden Sie "HS512" als Algorithmus. Sie erhalten die Meldung "Signatur verifiziert" (Assinatura verificada):
[
]
Um eine Zuordnung zu ermöglichen, muss Ihre Anwendung einen Chave bereitstellen. Dieser ermöglicht eine sichere Zuordnung - auch wenn der JWT dekodiert wird, bleibt die Zuordnung sicher gespeichert. Es wird dringend empfohlen, bei der Erstellung eines JWTs immer einen Senha zu verwenden.
Token-Typen
Uma vez que um JWT pode ser definido para expirar (ser invalidado) após um determinado período de tempo, dois tokens serão considerados neste pedido:
Zugangs-Token: Ein Zugriffstoken wird für Anfragen verwendet, die eine Autorisierung erfordern. Normalerweise wird es dem Antrag beigefügt. Es wird empfohlen, dass ein Token de acesso eine kurze Gültigkeitsdauer hat, z.B. 15 Minuten. Einem Token eine kurze Gültigkeitsdauer zu geben, kann einen schwerwiegenden Schaden verhindern, wenn der Token von einem Benutzer verfälscht wird, falls er gehackt wird. O hacker tem apenas 15 minutos ou menos para realizar suas operações antes que o token seja invalidado.
Token auffrischen: Ein Aktualisierungs-Token hat eine längere Nutzungsdauer, in der Regel 7 Tage. Dieses Token wird verwendet, um neue Zugangs- und Aktualisierungs-Token zu erhalten. Wenn die Zugriffsmarke abläuft, werden neue Zugriffs- und Aktualisierungsmarken erzeugt, sobald die Rota der Aktualisierungsmarke abläuft (ab unserer Anwendung).
Wie kann man ein JWT aufladen?
Für eine große Produktionsanwendung ist es sehr empfehlenswert, JWTs in einem Cookie zu speichern. HttpOnly. Dazu wird bei der Übermittlung des vom Backend erzeugten Cookies an das Frontend (Client) ein Flag HttpOnly enviada ao longo do cookie, instructo o navegador a não exibir o cookie através dos scripts do lado do cliente. So können XSS-Attacken (Cross Site Scripting) verhindert werden. O JWT kann auch bei der lokalen Speicherung des Webbrowsers oder bei der Speicherung der Sitzung gespeichert werden. Die Speicherung eines JWT auf diese Weise kann zu einer Vielzahl von Angriffen wie dem oben erwähnten XSS führen, was im Vergleich zur Verwendung des "HttpOnly-Cookies" generell weniger sicher ist.
Aplicação
Betrachten wir eine API Restful ToDo.
Einrichten eines Diretório chamado jwt-todo, depois inicializar go.mod für die Verfolgung der Abhängigkeiten. O go.mod é inicializado usando:
go mod init jwt-todoRufen Sie jetzt eine Anfrage main.go dentro do diretório raiz /jwt-todo, e adicione isto a ele:
package main
func main() {}Wir verwenden Gin für die Erstellung und Bearbeitung von HTTP-Anfragen. Das Gin Framework hilft bei der Reduzierung des Boilerplate-Dokuments und ist sehr effizient bei der Erstellung von APIs.
Você pode instalar o gin, se ainda não o fez, usando:
go get github.com/gin-gonicEm seguida, atualize o arquivo main.go:
package main
Import (
"github.com/gin-gonic/gin"
)
var (
router = gin.Default()
)
func main() {
router.POST("/login", Login)
log.Fatal(router.Run(":8080"))
}In einer idealen Situation misst eine Rota /login toma as credenciais de um usuário, compara-as com algum banco de dados, e as registra se as credenciais forem válidas. In dieser API verwenden wir nur eine Benutzergruppe, die wir im Speicher definieren. Rufen Sie einen Benutzerkonto in einer "Struktur" auf. Adicione isto ao arquivo main.go:
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
//A sample use
var user = User{
ID: 1,
Username: "username",
Password: "password",
} Login-Anfrage
Wenn die Daten eines Benutzers verifiziert sind, wird er angemeldet und ein JWT mit seinem Namen erstellt. Das erreichen wir mit der Funktion Login() die oben definiert ist:
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 empfangen die Anfrage des Benutzers und dessialisieren sie anschließend für die "Struktur" des Benutzers. Danach vergleichen wir den Benutzer bei der Eingabe mit dem, was wir im Speicher definiert haben. Se estivéssemos utilizando um banco de dados, teríamos comparado com um registro no banco de dados.
Um die Login-Funktion nicht ungenutzt zu lassen, wurde die Logik für die Erstellung eines JWTs von der CreateToken. Beachten Sie, dass eine Benutzeridentifikation für diese Funktion übergeben wird. Sie wird als erneute Bestätigung für das Abrufen eines JWTs verwendet.
A função CreateToken benutzt den Pacote dgrijalva/jwt-go, nós podemos instalar este assim:
go get github.com/dgrijalva/jwt-goVamos definir a função CreateToken:
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 festgelegt, dass der Token nur 15 Minuten lang gültig ist, und dass er, nachdem er ungültig geworden ist, nicht mehr für andere Authentifizierungsanfragen verwendet werden kann. Beachten Sie auch, dass wir den JWT mit einem von unserer Umgebungsvariable erhaltenen Passwort (ACCESS_SECRET) verknüpfen. Es ist sehr empfehlenswert, dass dieser Wert nicht in der Datenbank angezeigt wird, sondern einfach in der Umgebung, wie wir es hier tun. Sie können sie in einer .env, `.yml oder so, wie es für Sie funktioniert.
Jetzt, nosso arquivo main.go se parece com isto:
package main
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"time"
)
var (
router = gin.Default()
)
func main() {
router.POST("/login", Login)
log.Fatal(router.Run(":8080"))
}
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Phone string `json:"phone"`
}
var user = User{
ID: 1,
Username: "username",
Password: "password",
Phone: "49123454322", //this is a random number
}
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)
}
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
}
Jetzt können wir die Anwendung ausführen:
go run main.goJetzt können wir es ausprobieren und sehen, was wir erreicht haben! Rufen Sie Ihre bevorzugte API auf und klicken Sie auf den Endpunkt für die Anmeldung:
[
]
Wie oben zu sehen ist, haben wir einen JWT, der 15 Minuten dauern wird.
Schlupflöcher bei der Umsetzung
Sim, nós podemos fazer o login de um usuário e gerar um JWT, mas há muitos erros com a implementação acima:
O JWT só pode ser invalidado quando expirar. Eine große Einschränkung dabei ist, dass ein Benutzer sich anmelden und dann entscheiden kann, sofort zu gehen, aber das JWT des Benutzers bleibt gültig, bis die Ablaufzeit abgelaufen ist.
O JWT kann von einem Hacker missbraucht werden, ohne dass der Benutzer etwas dagegen tun kann, bis der Token abläuft.
O usuário precisará registrar-se novamente após a expiração do token, levando assim a uma má experiência do usuário.
Podemos resolver os problemas mencionados acima de duas maneiras:
Verwendung einer dauerhaften Archivierungskampagne für die Archivierung von JWT-Metadaten. Dies ermöglicht es, ein JWT-Logo in der Sekunde ungültig zu machen, in der sich der Benutzer abmeldet, und erhöht so die Sicherheit.
Mit dem Konzept der Token-Aktualisierung kann ein neuer Token für den Fall, dass der Token abläuft, erstellt werden, um so die Benutzererfahrung zu verbessern.
Verwendung von Redis zur Archivierung von JWT-Metadaten
Eine der Lösungen, die wir Ihnen anbieten, ist die Verwendung von JWT-Metadaten in einer Dauerwerbesendung. Dies kann für jede beliebige Dauercamada erfolgen, aber die Verwendung von Redis ist unbedingt zu empfehlen. Uma vez que os JWTs que geramos têm tempo de expiração, o redis tem uma característica que elimina automaticamente os dados cujo tempo de expiração foi atingido. O redis também pode manipular muitas escritas e pode escalar horizontalmente.
Da es sich bei dem Redis um einen Speicher vom Typ Key-Value handelt, müssen die Chaves eindeutig sein. Um dies zu erreichen, verwenden wir uuid als Chave und id do usuário als Valor.
Portanto, vamos instalar dois pacotes para usar:
go get github.com/go-redis/redis/v7
go get github.com/twinj/uuidTambém importaremos os que estão no arquivo main.go so:
import (
…
"github.com/go-redis/redis/v7"
"github.com/twinj/uuid"
…
)Hinweis: Ich erwarte, dass Sie Redis auf Ihrem lokalen Gerät installiert haben. Im gegenteiligen Fall können Sie die Installation abbrechen und beenden, bevor Sie fortfahren.
Vamos agora inicializar o redis:
var client *redis.Client
func init() {
//Initializing redis
dsn := os.Getenv("REDIS_DSN")
if len(dsn) == 0 {
dsn = "localhost:6379"
}
client = redis.NewClient(&redis.Options{
Addr: dsn, //redis port
})
_, err := client.Ping().Result()
if err != nil {
panic(err)
}
}
O cliente redis é inicializado na função init(). Dadurch wird sichergestellt, dass jedes Mal, wenn wir eine Anfrage ausführen main.goausgeführt wird, wird der Redis automatisch verbunden.
Wenn wir von diesem Punkt aus ein Token erstellen, erhalten wir eine ID, die als einer der Token-Claims verwendet wird, genauso wie wir die ID des Nutzers als einen Claim bei der vorherigen Implementierung verwenden.
Definir os Metadados=
In unserer vorgeschlagenen Lösung müssen wir statt eines Tokens zwei JWTs erstellen:
O token de acesso
O Token Refresh
Dazu ist es notwendig, eine Struktur zu definieren, die diese Token-Definitionen, ihre Gültigkeitsdauer und UUIDS zusammenfasst:
type TokenDetails struct {
AccessToken string
RefreshToken string
AccessUuid string
RefreshUuid string
AtExpires int64
RtExpires int64
}O prazo de validade e os uuids são muito úteis porque serão usados ao salvar metadados simbólicos em redis.
Jetzt werden wir eine Funktion aktualisieren CreateToken para ter este aspecto:
func CreateToken(userid uint64) (*TokenDetails, error) {
td := &TokenDetails{}
td.AtExpires = time.Now().Add(time.Minute * 15).Unix()
td.AccessUuid = uuid.NewV4().String()
td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
td.RefreshUuid = uuid.NewV4().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 dieser Funktion läuft das Zugriffsrecht nach 15 Minuten und die Auffrischung des Tokens nach 7 Tagen ab. Sie können auch beobachten, dass wir zu jedem Token eine Kennung als Anspruch hinzufügen.
Da die UID jedes Mal, wenn sie erstellt wird, einzigartig ist, kann ein Benutzer mehr als ein Token erstellen. Dies ist der Fall, wenn ein Benutzer bei verschiedenen Geräten angemeldet ist. O usuário também pode fazer logout de qualquer um dos dispositivos sem que eles sejam desconectados de todos os dispositivos. Que legal!
Salvando metadados de JWTs
Wir werden jetzt eine Funktion einrichten, die für die Speicherung von JWT-Metadaten verwendet werden kann:
func CreateAuth(userid uint64, td *TokenDetails) error {
at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object)
rt := time.Unix(td.RtExpires, 0)
now := time.Now()
errAccess := client.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err()
if errAccess != nil {
return errAccess
}
errRefresh := client.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err()
if errRefresh != nil {
return errRefresh
}
return nil
}Passamos no TokenDetails die Informationen über das Ablaufdatum von JWTs und die bei der Erstellung von JWTs genutzten Benutzerdaten enthält. Wenn die Gültigkeitsdauer sowohl für das Aktualisierungs- als auch für das Zugriffstoken abgelaufen ist, wird das JWT automatisch aus dem Redis ausgeschlossen.
Ich benutze Redily, eine Benutzeroberfläche für Redakteure. É uma boa ferramenta. Sie können einen Blick darauf werfen, um zu sehen, wie die JWT-Metadaten im Key-Value-Paradigma gespeichert werden.
[
]
Bevor wir das neue Login testen, müssen wir die Funktion CreateAuth() na função Login(). Aktualisieren Sie die Funktion Login:
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
}
ts, err := CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, err.Error())
return
}
saveErr := CreateAuth(user.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, neu in das System einzutreten. Salve o arquivo main.go e execute-o. Quando o login for atingido pelo Postman, devemos ter feito:
[
]
Großartig! Wir haben sowohl access_token als auch refresh_token, und wir haben auch einfache Metadaten, die in Redis bestehen.
Criando um Todo
Nun können wir Anfragen, die eine Authentifizierung erfordern, mit Hilfe von JWT bearbeiten.
Eine der Anfragen, die in dieser API nicht autentisch sind, ist die Erstellung einer gesamten Anfrage.
Primeiro vamos definir um struct Todo:
type Todo struct {
UserID uint64 `json:"user_id"`
Title string `json:"title"`
}Bei der Ausführung einer autorisierten Anfrage müssen wir den Token, der im Authentifizierungssystem übergeben wird, validieren, um zu sehen, ob er gültig ist. Wir müssen einige Hilfsfunktionen für diese Token definieren.
Zunächst müssen wir das Token des Anforderungscodes mit einer Funktion extrahieren ExtractToken:
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 überprüfen wir den Token:
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
}Chamamos o ExtractToken innerhalb der Funktion VerifyToken um den Token-String zu erhalten, danach verifizieren wir den Assinaturmodus.
Anschließend überprüfen wir die Gültigkeit dieses Tokens, ob er noch gültig ist oder bereits abgelaufen ist, indem wir die Funktion TokenValid:
func TokenValid(r *http.Request) error {
token, err := VerifyToken(r)
if err != nil {
return err
}
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
return err
}
return nil
}
Außerdem wollen wir die Metadaten des Tokens extrahieren, die in unserem zuvor montierten Store Redis erworben wurden. Um den Token zu extrahieren, definieren wir eine Funktion ExtractTokenMetadata:
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
}
A função ExtractTokenMetadata retorna um AccessDetails (que é um struct). Este struct enthält die Metadaten (access_uuid e user_id), die wir für eine Suche in der Datenbank benötigen. Wenn Sie einen Grund haben, warum wir die Metadaten dieses Tokens nicht erhalten, wird die Anfrage mit einer Fehlermeldung unterbrochen.
O struct AccessDetails mencionado acima se parece com isto:
type AccessDetails struct {
AccessUuid string
UserId uint64
}Wir haben auch die Suche nach Metadaten für Token in Redaktionen erwähnt. Wir werden eine Funktion definieren, die es uns ermöglicht, dies zu tun:
func FetchAuth(authD *AccessDetails) (uint64, error) {
userid, err := client.Get(authD.AccessUuid).Result()
if err != nil {
return 0, err
}
userID, _ := strconv.ParseUint(userid, 10, 64)
return userID, nil
}FetchAuth() aceita os AccessDetails da função ExtractTokenMetadataund ruft dann den Redis auf. Wenn das Register nicht gefunden wird, kann dies bedeuten, dass der Token abgelaufen ist oder ein Fehler aufgetreten ist.
Vamos finalmente ligar a função CreateTodo um eine bessere Umsetzung dieser Funktionen 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 wir gesehen haben, haben wir die ExtractTokenMetadata um die JWT-Metadaten zu extrahieren, die in FetchAuth um zu überprüfen, ob die Metadaten in unserem Redis-Speicher vorhanden sind. Se tudo estiver bem, o Todo pode então ser salvo no banco de dados, mas optamos por devolvê-lo ao caller.
Vamos atualizar o main() para incluir a função CreateTodo:
func main() {
router.POST("/login", Login)
router.POST("/todo", CreateTodo)
log.Fatal(router.Run(":8080"))
}Zum Testen von o CreateTodozu testen, loggen Sie sich ein und kopieren Sie o access_token und adicione-o ao campo do "Bearer Token Field" como este:
[
]
Anschließend fügen Sie dem Anforderungskörper einen Titel hinzu, um eine Anfrage zu erstellen und eine POST-Anforderung an den Endpunkt zu senden /todowie im Folgenden beschrieben:
[
]
Ein Versuch, einen ToDo ohne Zugang zu erstellen, wird abgelehnt:
[
]
Antrag auf Abmeldung
Bis jetzt haben wir gesehen, wie ein JWT verwendet wird, um eine authentische Anfrage zu stellen. Wenn ein Benutzer sich abmeldet, revogamos/invalidamos instantaneamente seu JWT. Dies ist möglich, indem wir die JWT-Metadaten unseres Redis-Stores anpassen.
Wir werden jetzt eine Funktion definieren, die es uns ermöglicht, JWT-Metadaten aus dem Redis auszuschließen:
func DeleteAuth(givenUuid string) (int64,error) {
deleted, err := client.Del(givenUuid).Result()
if err != nil {
return 0, err
}
return deleted, nil
}A função acima apagará o registro no redis que correspondonde ao uuid passado como parâmetro.
A função Logout tem este aspecto:
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 { //if any goes wrong
c.JSON(http.StatusUnauthorized, "unauthorized")
return
}
c.JSON(http.StatusOK, "Successfully logged out")
}Na função Logoutextraímos primeiro os metadados do JWT. Wenn dies gelingt, gehen wir zur Eliminierung dieser Metadaten über und machen das JWT sofort unbrauchbar.
Aktualisieren Sie vor dem Testen das Dokument main.go und fügen Sie den Endpunkt logout wie diesen:
func main() {
router.POST("/login", Login)
router.POST("/todo", CreateTodo)
router.POST("/logout", Logout)
log.Fatal(router.Run(":8080"))
}Forneça um access_token válido associado a um usuário e, em seguida, faça logout do usuário. Lembre-se de adicionar o access_token ao Authorization Bearer Token und erreichen Sie anschließend den Endpunkt der Abmeldung:
[
]
Jetzt ist der Benutzer abgemeldet, und keine weitere Anfrage kann mit diesem JWT neu gestellt werden, wenn es sofort ungültig wird. Diese Implementierung ist sicherer als die Erwartung, dass ein JWT abläuft, sobald ein Benutzer aus dem System entfernt wird.
Protegendo Rotas Autenticadas
Temos duas rotas que requerem autenticação:/login e /logout. Heute kann jede Person, ob mit oder ohne Berechtigung, auf diese Dienste zugreifen. Vamos mudar isso.
Precisaremos definir a função TokenAuthMiddleware() um diese Rotationen zu gewährleisten:
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 zu sehen, verwenden wir eine Funktion TokenValid() (die zuvor definiert wurde), um zu überprüfen, ob der Token noch gültig ist oder abläuft. Diese Funktion wird für die zum Schutz der Token definierten Rollen verwendet.
Vamos agora atualizar o main.go um diese Middleware einzubeziehen:
func main() {
router.POST("/login", Login)
router.POST("/todo", TokenAuthMiddleware(), CreateTodo)
router.POST("/logout", TokenAuthMiddleware(), Logout)
log.Fatal(router.Run(":8080"))
} Atulizando os Tokens
Im Moment können wir JWTs erstellen, verwenden und revidieren. Was passiert bei einer Anwendung, die eine Benutzerschnittstelle enthält, wenn der Token für den Zugang abläuft und der Benutzer einen Antrag stellen muss? Wird der Benutzer desautorisiert und muss er sich neu anmelden? Unglücklicherweise ist dies der Fall. Dies kann jedoch mit dem Konzept eines Refresh-Tokens vermieden werden. O usuário não precisa fazer o login novamente.
Das Refresh-Token, das zusammen mit dem Zugriffstoken erstellt wird, wird verwendet, um neue Paare von Zugriffs- und Refresh-Tokens zu erstellen.
Durch die Verwendung von JavaScript zur Nutzung unserer API-Terminals können wir die JWTs mit Hilfe von axialen Interceptoren leicht aktualisieren. In unserer API müssen wir eine POST-Anfrage mit einem refresh_token als Kernstück für den Endpunkt /token/refresh.
Vamos primeiro criar a função Refresh():
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") //this should be in an env file
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 _, ok := token.Claims.(jwt.Claims); !ok && !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")
}
}
Enquanto muita coisa está acontecendo nessa função, vamos tentar entender o fluxo.
Primeiro tomamos o
refresh_tokendo corpo de request.Im Anschluss daran überprüfen wir die Vorgehensweise bei der Zusammenstellung des Tokens.
Anschließend prüfen wir, ob das Token noch gültig ist.
O
refresh_uuidund ouser_idsind daher extraídos, die als Metadaten verwendet werden, um ein Token für die Aktualisierung zu erstellen.Danach haben wir die Metadaten aus dem Redis Store geholt und mit dem
refresh_uidcomo chave.Danach werden wir ein neues Paket von Zugangs- und Auffrischungstoken erstellen, die jetzt für künftige Anfragen verwendet werden.
Die Metadaten der Zugriffs- und Aktualisierungs-Token sind im Redis enthalten.
Os tokens criados são devolvidos ao caller.
Wenn der Aktualisierungs-Token nicht gültig ist, erhält der Benutzer keine Erlaubnis, ein neues Token-Paket zu erstellen. Wir müssen eine neue Anmeldung durchführen, um neue Token zu erhalten.
Anschließend ist ein Aktualisierungsplan für Token in die Funktion "Token" einzufügen. main():
router.POST("/token/refresh", Refresh)Testando o empoint com um refresh_token válido:
[![testing}(https://www.nexmo.com/wp-content/uploads/2020/03/image7.png)]
Wir haben mit Erfolg neue Ficha-Pakete erstellt. Beleza!! 😎.
Senden von Nachrichten mit der API für Nachrichten von Vonage
Wir benachrichtigen unsere Kunden, sobald sie mit der API von Mensagens Vonage ein ToDo erstellt haben.
Sie können Ihren API-Schlüssel und Ihren Senha in einer variablen Umgebung definieren und dann in dieser Form in diesem Formular verwenden:
var (
NEXMO_API_KEY = os.Getenv( "your_api_key")
NEXMO_API_SECRET = os.Getenv("your_secret")
)Anschließend definieren wir einige Strukturen, die Informationen über den Empfänger, den Empfänger und den Inhalt der Nachricht enthalten:
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"`
}Danach definieren wir eine Funktion, um eine Nachricht an einen anderen Benutzer zu senden:
func SendMessage(username, phone string) (*http.Response, error) {
data := Payload{
From: From{
Type: "sms",
Number: "Nexmo",
},
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(NEXMO_API_KEY, NEXMO_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 ersten Funktion ist die Nummer To die Nummer des Benutzers, während die Nummer From über die Kontrollleiste von Vonage eingegeben werden muss.
Bestätigen Sie, dass Sie Ihre Variablen haben NEXMO_API_KEY e NEXMO_API_SECRET definidas em seu arquivo de variáveis de ambiente.
Wir aktualisieren daraufhin eine Funktion CreateTodo para incluir a função SendMessage que acabou de ser definida, passando nos parâmetros necessários:
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:
//Send the user a notification
msgResp, err := SendMessage(user.Username, user.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
}
c.JSON(http.StatusCreated, td)
}
Vergewissern Sie sich, dass eine gültige Telefonnummer vergeben wurde, damit Sie eine Nachricht erhalten können, wenn Sie versuchen, eine Nachricht zu verfassen.
Schlussfolgerung
Sie sehen, wie Sie eine JWT erstellen und ungültig machen können. Außerdem erfahren Sie, wie Sie die Vonage Messages API in Ihre Golang-Anwendung integrieren können, um Benachrichtigungen zu versenden. Weitere Informationen über die besten Vorgehensweisen und die Verwendung von JWT finden Sie in diesem repo do GitHub. Sie können diese Anwendung installieren und eine echte Datenbank verwenden, um Benutzer und andere Personen zu speichern, und Sie können auch React oder Vue.js verwenden, um ein Frontend zu erstellen. Hier werden Sie das Refresh Token mit Hilfe von Axios Interceptors wirklich zu schätzen wissen.
Teilen Sie:
Victor Steven is a self-taught full-stack developer that loves researching about doing things differently. He has a degree in Engineering and over 5 years of experience as a software developer. Steven is very interested in designing and building scalable APIs and he enjoys writing about his discoveries in software development.