Usare JWT in Go senza framework
In un precedente articolo abbiamo visto come creare una web api in Go senza framework.
In questo articolo vediamo come usare JWT per l'autenticazione e la protezione delle rotte.
Queste le librerie che ci servono:
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt
go get -u github.com/go-sql-driver/mysql
Date anche questo comando:
go mod tidy
Questa la tabella che ho usato per salvare gli utenti:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
Qui sotto il codice; ho messo tutto insieme per comodità, ma sarebbe meglio dividerlo:
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var jwtSecret = []byte("tua-chiave-segreta-super-sicura")
type Persona struct {
ID int `json:"id"`
Email string `json:"email"`
}
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
User User `json:"user"`
}
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
var db *sql.DB
func initDB() error {
var err error
dsn := "root:9211@tcp(localhost:3306)/test"
db, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("errore apertura database: %v", err)
}
if err = db.Ping(); err != nil {
return fmt.Errorf("errore connessione database: %v", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
log.Println("Connessione al database MariaDB riuscita")
return nil
}
// Genera un token JWT
func generateJWT(userID int, username string) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "api-persone",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func validateJWT(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("metodo di firma non valido")
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("token non valido")
}
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
respondJSON(w, http.StatusUnauthorized, Response{
Success: false,
Error: "Token di autenticazione mancante",
})
return
}
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
respondJSON(w, http.StatusUnauthorized, Response{
Success: false,
Error: "Formato token non valido",
})
return
}
claims, err := validateJWT(tokenParts[1])
if err != nil {
respondJSON(w, http.StatusUnauthorized, Response{
Success: false,
Error: "Token non valido o scaduto",
})
return
}
r.Header.Set("X-User-ID", fmt.Sprintf("%d", claims.UserID))
r.Header.Set("X-Username", claims.Username)
next(w, r)
}
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
respondJSON(w, http.StatusMethodNotAllowed, Response{
Success: false,
Error: "Solo POST è supportato",
})
return
}
var loginReq LoginRequest
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "JSON non valido",
})
return
}
var user User
var hashedPassword string
err := db.QueryRow("SELECT id, username, password FROM users WHERE username = ?", loginReq.Username).Scan(&user.ID, &user.Username, &hashedPassword)
if err == sql.ErrNoRows {
respondJSON(w, http.StatusUnauthorized, Response{
Success: false,
Error: "Credenziali non valide",
})
return
}
if err != nil {
log.Printf("Errore query: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore del server",
})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(loginReq.Password)); err != nil {
respondJSON(w, http.StatusUnauthorized, Response{
Success: false,
Error: "Credenziali non valide",
})
return
}
token, err := generateJWT(user.ID, user.Username)
if err != nil {
log.Printf("Errore generazione token: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante la generazione del token",
})
return
}
respondJSON(w, http.StatusOK, Response{
Success: true,
Data: LoginResponse{
Token: token,
User: user,
},
})
}
func registerHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
respondJSON(w, http.StatusMethodNotAllowed, Response{
Success: false,
Error: "Solo POST è supportato",
})
return
}
var loginReq LoginRequest
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "JSON non valido",
})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(loginReq.Password), bcrypt.DefaultCost)
if err != nil {
log.Printf("Errore hash password: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante la registrazione",
})
return
}
result, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", loginReq.Username, string(hashedPassword))
if err != nil {
log.Printf("Errore insert: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Username già esistente o errore del server",
})
return
}
userID, _ := result.LastInsertId()
respondJSON(w, http.StatusCreated, Response{
Success: true,
Data: map[string]interface{}{
"id": userID,
"username": loginReq.Username,
"message": "Registrazione completata con successo",
},
})
}
func personeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
path := strings.TrimPrefix(r.URL.Path, "/persone")
switch r.Method {
case "GET":
if path == "" || path == "/" {
handleGetAllPersone(w, r)
} else {
id := strings.TrimPrefix(path, "/")
handleGetPersona(w, r, id)
}
case "POST":
handleCreatePersona(w, r)
case "PUT":
id := strings.TrimPrefix(path, "/")
handleUpdatePersona(w, r, id)
case "DELETE":
id := strings.TrimPrefix(path, "/")
handleDeletePersona(w, r, id)
default:
respondJSON(w, http.StatusMethodNotAllowed, Response{
Success: false,
Error: "Metodo non supportato",
})
}
}
func handleGetAllPersone(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, email FROM persone ORDER BY id")
if err != nil {
log.Printf("Errore query: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante il recupero dei dati",
})
return
}
defer rows.Close()
persone := []Persona{}
for rows.Next() {
var p Persona
if err := rows.Scan(&p.ID, &p.Email); err != nil {
log.Printf("Errore scan: %v", err)
continue
}
persone = append(persone, p)
}
respondJSON(w, http.StatusOK, Response{Success: true, Data: persone})
}
func handleGetPersona(w http.ResponseWriter, r *http.Request, id string) {
var p Persona
err := db.QueryRow("SELECT id, email FROM persone WHERE id = ?", id).Scan(&p.ID, &p.Email)
if err == sql.ErrNoRows {
respondJSON(w, http.StatusNotFound, Response{
Success: false,
Error: "Persona non trovata",
})
return
}
if err != nil {
log.Printf("Errore query: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante il recupero dei dati",
})
return
}
respondJSON(w, http.StatusOK, Response{Success: true, Data: p})
}
func handleCreatePersona(w http.ResponseWriter, r *http.Request) {
var p Persona
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "JSON non valido",
})
return
}
if p.Email == "" {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "Email obbligatoria",
})
return
}
result, err := db.Exec("INSERT INTO persone (email) VALUES (?)", p.Email)
if err != nil {
log.Printf("Errore insert: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante la creazione",
})
return
}
lastID, _ := result.LastInsertId()
p.ID = int(lastID)
respondJSON(w, http.StatusCreated, Response{Success: true, Data: p})
}
func handleUpdatePersona(w http.ResponseWriter, r *http.Request, id string) {
var p Persona
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "JSON non valido",
})
return
}
if p.Email == "" {
respondJSON(w, http.StatusBadRequest, Response{
Success: false,
Error: "Email obbligatoria",
})
return
}
result, err := db.Exec("UPDATE persone SET email = ? WHERE id = ?", p.Email, id)
if err != nil {
log.Printf("Errore update: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante l'aggiornamento",
})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
respondJSON(w, http.StatusNotFound, Response{
Success: false,
Error: "Persona non trovata",
})
return
}
fmt.Sscanf(id, "%d", &p.ID)
respondJSON(w, http.StatusOK, Response{Success: true, Data: p})
}
func handleDeletePersona(w http.ResponseWriter, r *http.Request, id string) {
result, err := db.Exec("DELETE FROM persone WHERE id = ?", id)
if err != nil {
log.Printf("Errore delete: %v", err)
respondJSON(w, http.StatusInternalServerError, Response{
Success: false,
Error: "Errore durante l'eliminazione",
})
return
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
respondJSON(w, http.StatusNotFound, Response{
Success: false,
Error: "Persona non trovata",
})
return
}
respondJSON(w, http.StatusOK, Response{
Success: true,
Data: "Persona eliminata con successo",
})
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
respondJSON(w, http.StatusOK, Response{
Success: true,
Data: "API con autenticazione JWT. Usa /login per autenticarti e /persone per gestire le persone",
})
}
func main() {
if err := initDB(); err != nil {
log.Fatalf("Impossibile connettersi al database: %v", err)
}
defer db.Close()
http.HandleFunc("/", rootHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/register", registerHandler)
http.HandleFunc("/persone", authMiddleware(personeHandler))
http.HandleFunc("/persone/", authMiddleware(personeHandler))
port := ":8080"
fmt.Printf("Server in ascolto su http://localhost%s\n", port)
fmt.Println("Endpoints disponibili:")
fmt.Println(" POST http://localhost:8080/register (pubblico)")
fmt.Println(" POST http://localhost:8080/login (pubblico)")
fmt.Println(" GET http://localhost:8080/persone (protetto)")
fmt.Println(" GET http://localhost:8080/persone/:id (protetto)")
fmt.Println(" POST http://localhost:8080/persone (protetto)")
fmt.Println(" PUT http://localhost:8080/persone/:id (protetto)")
fmt.Println(" DELETE http://localhost:8080/persone/:id (protetto)")
log.Fatal(http.ListenAndServe(port, nil))
}
Qui un pò di comandi per i test:
$ curl -X POST http://localhost:8080/register -H "Content-Type: application/json" -d '{"username":"mario","password":"password123"}'
$ curl -X POST http://localhost:8080/login -H "Content-Type: application/json" -d '{"username":"mario","password":"password123"}'
$ curl http://localhost:8080/persone -H "Authorization: Bearer TOKEN_SBAGLIATO"
$ curl http://localhost:8080/persone -H "Authorization: Bearer TOKEN_CORRETTO"
Enjoy!
go mysql jwt bcrypt
Commentami!