Usare JWT in Gin Gonic

Mattepuffo's logo
Usare JWT in Gin Gonic

Usare JWT in Gin Gonic

In questo articolo vediamo come usare JWT in Gin Gonic.

Oltre alla libreria apposita, io ho usato anche cors e gorm.

Per quanto riguarda JWT, installiamo il package così:

$ go get -u github.com/golang-jwt/jwt/v5
$ go mod tidy

Fatto questo cominciamo dal model per la tabella utenti:

package models

import "time"

type Utente struct {
	ID           int32     `json:"ut_id" gorm:"column:ut_id;primaryKey"`
	User         string    `json:"ut_user" gorm:"column:ut_user;unique"`
	Password     string    `json:"ut_password" gorm:"column:ut_password"`
	DataReg      time.Time `json:"ut_data_reg" gorm:"column:ut_data_reg"`
	DataLogin    time.Time `json:"ut_data_login" gorm:"column:ut_data_login"`
	DataModifica time.Time `json:"ut_data_modifica" gorm:"column:ut_data_modifica"`
	TokenJwt     string    `json:"ut_token"`
}

func (Utente) TableName() string {
	return "todo_utenti"
}

Nel mio caso ho dovuto "forzare" il nome della tabella, in quanto già esistente e non creata con gorm.

Questo il controller con la funzione per il login:

package controllers

import (
	"net/http"
	"os"
	"strings"
	"time"
	"todo/models"
	"todo/utils"

	"github.com/MakeNowJust/heredoc"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
)

func Login(ctx *gin.Context) {
	var item models.Utente
	var response utils.CustomResponse

	if err := ctx.BindJSON(&item); err != nil {
		response.Res = "ko"
		response.Message = "Si è verificato un errore!"
		response.Error = err.Error()
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	} else {
		var user string = strings.TrimSpace(strings.ToLower(item.User))
		var password string = item.Password

		models.DB.Where("ut_user = ?", user).Where("ut_password = ?", password).First(&item)

		if item.ID > 0 {
			token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
				"exp": time.Now().AddDate(1, 0, 0).Unix(),
				"sub": item.ID,
			})

			tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))

			if err != nil {
				response.Res = "ko"
				response.Message = "Si è verificato un errore!"
				response.Error = err.Error()
				response.Last = 0

				ctx.JSON(http.StatusOK, gin.H{"data": response})
				ctx.Done()
			}

			item.TokenJwt = tokenString

			strQuery := heredoc.Doc(`
				UPDATE todo_utenti
				SET ut_data_login = ?
				WHERE ut_user = ?
			`)

			models.DB.Exec(strQuery, time.Now(), item.ID)

			ctx.JSON(http.StatusOK, gin.H{"data": item})
			ctx.Done()
		} else {
			response.Res = "ko"
			response.Message = "Si è verificato un errore!"
			response.Error = "Credenziali errate!"
			response.Last = 0

			ctx.JSON(http.StatusOK, gin.H{"data": response})
			ctx.Done()
		}
	}

}

Nel mio caso c'è anche l'update della data per ultimo login; magari a voi non serve.

Questo il middleware:

package middleware

import (
	"errors"
	"net/http"
	"os"
	"strings"
	"time"
	"todo/models"
	"todo/utils"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
)

func CheckToken(ctx *gin.Context) {
	tokenString, err := extractBearerToken(ctx.GetHeader("Authorization"))

	var response utils.CustomResponse

	if err != nil {
		response.Res = "ko"
		response.Message = "Attenzione!"
		response.Error = "Non sei autorizzato"
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	}

	token, err := parseToken(tokenString)

	if err != nil {
		response.Res = "ko"
		response.Message = "Attenzione!"
		response.Error = "Non sei autorizzato"
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	}

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

	if !ok {
		response.Res = "ko"
		response.Message = "Attenzione!"
		response.Error = "Non sei autorizzato"
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	}

	if float64(time.Now().Unix()) > claims["exp"].(float64) {
		response.Res = "ko"
		response.Message = "Attenzione!"
		response.Error = "Non sei autorizzato"
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	}

	var user models.Utente
	models.DB.First(&user, claims["sub"])

	if user.ID == 0 {
		response.Res = "ko"
		response.Message = "Attenzione!"
		response.Error = "Non sei autorizzato"
		response.Last = 0

		ctx.JSON(http.StatusOK, gin.H{"data": response})
		ctx.Done()
	}

	ctx.Set("user", user)
	ctx.Next()
}

func extractBearerToken(header string) (string, error) {
	if header == "" {
		return "", errors.New("bad header value given")
	}

	jwtToken := strings.Split(header, " ")
	if len(jwtToken) != 2 {
		return "", errors.New("incorrectly formatted authorization header")
	}

	return jwtToken[1], nil
}

func parseToken(jwtToken string) (*jwt.Token, error) {
	token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
		if _, OK := token.Method.(*jwt.SigningMethodHMAC); !OK {
			return nil, errors.New("bad signed method received")
		}
		return []byte(os.Getenv("SECRET")), nil
	})

	if err != nil {
		return nil, errors.New("bad jwt token")
	}

	return token, nil
}

La funzione CheckToken è quella che verrà richiamata dalle rotte.

Il SECRET lo impostiamo nel main, ma sarebbe meglio metterlo in un file:

package main

import (
	"fmt"
	"os"
	"todo/controllers"
	"todo/middleware"
	"todo/models"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {
	os.Setenv("SECRET", "SECRET")

	router := gin.Default()
	router.SetTrustedProxies(nil)

	config := cors.DefaultConfig()
	config.AllowAllOrigins = true
	config.AllowMethods = []string{"POST", "GET", "PUT", "OPTIONS"}
	config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization", "Accept", "User-Agent", "Cache-Control", "Pragma"}
	config.ExposeHeaders = []string{"Content-Length"}
	config.AllowCredentials = true
	router.Use(cors.New(config))

	models.ConnectDatabase()

	router.GET("/", func(ctx *gin.Context) {
		ctx.JSON(200, gin.H{
			"message": "Beviti un GIN!",
		})
	})

	clientiRoute := router.Group("clienti")
	{
		clientiRoute.GET("/", middleware.CheckToken, controllers.GetClienti)
	}

	utentiRoute := router.Group("utenti")
	{
		utentiRoute.POST("login", controllers.Login)
		utentiRoute.POST("register", controllers.Register)
	}

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	fmt.Println("Server in asolto su http://localhost:" + port)

	if err := router.Run(":" + port); err != nil {
		os.Exit(1)
	}
}

Per completezza vi metto qua sotto anche CustomReponse che avete in giro:

package utils

type CustomResponse struct {
	Res     string `json:"res"`
	Message string `json:"message"`
	Error   string `json:"error"`
	Last    int32  `json:"last"`
}

ConnectDatabase invece inizializza la connessione:

package models

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDatabase() {
	dsn := "........"

	database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		fmt.Println(err)
	}

	DB = database
}

Direi che è tutto!

Enjoy!


Condividi

Commentami!