Manual para Crear una REST API en Go con Bcrypt, JWT y Swagger

Tabla de Contenidos

1. Introducción

Este manual detalla cómo construir una API RESTful segura en Go que incluye:

Requisitos previos:

2. Configuración del Proyecto

2.1 Inicializar el proyecto

mkdir go-rest-api
cd go-rest-api
go mod init github.com/tu-usuario/go-rest-api

2.2 Dependencias necesarias

Edita el archivo go.mod o ejecuta:

go get github.com/gin-gonic/gin           # Framework web
go get golang.org/x/crypto/bcrypt      # Para hashing de contraseñas
go get github.com/golang-jwt/jwt/v4    # Para JWT
go get github.com/swaggo/swag/cmd/swag # Generador Swagger
go get github.com/swaggo/gin-swagger   # Integración Swagger con Gin
go install github.com/swaggo/swag/cmd/swag@latest

3. Estructura del Proyecto

go-rest-api/
├── cmd/
│   └── server/
│       └── main.go          # Punto de entrada
├── internal/
│   ├── auth/                # Lógica de autenticación
│   │   ├── handler.go       # Manejadores de auth
│   │   ├── service.go       # Lógica de negocio
│   │   └── repository.go    # Acceso a datos
│   ├── user/                # Gestión de usuarios
│   ├── config/              # Configuración
│   ├── middleware/          # Middlewares
│   └── models/              # Modelos de datos
├── pkg/
│   ├── database/            # Conexión a DB
│   └── utils/               # Utilidades
├── docs/                    # Documentación Swagger
├── .env                     # Variables de entorno
└── Makefile                 # Automatización

4. Sistema de Autenticación

4.1 Modelo de Usuario

// internal/models/user.go
package models

import "time"

type User struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Username  string    `json:"username" gorm:"unique;not null"`
    Email     string    `json:"email" gorm:"unique;not null"`
    Password  string    `json:"-" gorm:"not null"` // No se serializa
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

4.2 Hash de contraseñas con Bcrypt

// internal/auth/service.go
package auth

import "golang.org/x/crypto/bcrypt"

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

4.3 Generación y validación de JWT

// internal/auth/service.go
import (
    "time"
    "github.com/golang-jwt/jwt/v4"
)

var jwtKey = []byte("tu_super_secreto_jwt") // Debería estar en variables de entorno

type Claims struct {
    UserID uint `json:"user_id"`
    jwt.StandardClaims
}

func GenerateToken(userID uint) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)
    
    claims := &Claims{
        UserID: userID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expirationTime.Unix(),
            Issuer:    "go-rest-api",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

func ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}
    
    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return jwtKey, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if !token.Valid {
        return nil, jwt.ErrSignatureInvalid
    }
    
    return claims, nil
}

4.4 Manejador de Registro y Login

// internal/auth/handler.go
package auth

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

type AuthHandler struct {
    service AuthService
}

func NewAuthHandler(service AuthService) *AuthHandler {
    return &AuthHandler{service: service}
}

type RegisterRequest struct {
    Username string `json:"username" binding:"required"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8"`
}

func (h *AuthHandler) Register(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // Verificar si el usuario ya existe
    if _, err := h.service.GetUserByEmail(req.Email); err == nil {
        c.JSON(http.StatusConflict, gin.H{"error": "email ya registrado"})
        return
    }
    
    // Hashear contraseña
    hashedPassword, err := HashPassword(req.Password)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "error al hashear contraseña"})
        return
    }
    
    // Crear usuario
    user := models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
    }
    
    if err := h.service.CreateUser(&user); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "error al crear usuario"})
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{"message": "usuario creado exitosamente"})
}

type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required"`
}

func (h *AuthHandler) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // Obtener usuario
    user, err := h.service.GetUserByEmail(req.Email)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "credenciales inválidas"})
        return
    }
    
    // Verificar contraseña
    if !CheckPasswordHash(req.Password, user.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "credenciales inválidas"})
        return
    }
    
    // Generar token JWT
    token, err := GenerateToken(user.ID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "error al generar token"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"token": token})
}

5. Definición de Rutas Protegidas

5.1 Configuración del Router

// cmd/server/main.go
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/tu-usuario/go-rest-api/internal/auth"
    "github.com/tu-usuario/go-rest-api/internal/middleware"
    "github.com/tu-usuario/go-rest-api/pkg/database"
    "gorm.io/gorm"
)

var db *gorm.DB

func main() {
    // Configuración inicial
    r := gin.Default()
    
    // Conexión a la base de datos
    db = database.ConnectDB()
    
    // Inicializar servicios
    authRepo := auth.NewAuthRepository(db)
    authService := auth.NewAuthService(authRepo)
    authHandler := auth.NewAuthHandler(authService)
    
    // Rutas públicas
    public := r.Group("/api/v1")
    {
        public.POST("/register", authHandler.Register)
        public.POST("/login", authHandler.Login)
    }
    
    // Rutas protegidas
    protected := r.Group("/api/v1")
    protected.Use(middleware.JWTAuthMiddleware())
    {
        protected.GET("/profile", func(c *gin.Context) {
            userID := c.GetUint("userID")
            // Lógica para obtener perfil...
        })
    }
    
    // Iniciar servidor
    r.Run(":8080")
}

6. Middleware JWT

// internal/middleware/auth.go
package middleware

import (
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
    "github.com/tu-usuario/go-rest-api/internal/auth"
    "net/http"
    "strings"
)

func JWTAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Obtener token del header Authorization
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "autorización requerida"})
            return
        }
        
        // Formato esperado: "Bearer "
        tokenParts := strings.Split(authHeader, " ")
        if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "formato de token inválido"})
            return
        }
        
        tokenString := tokenParts[1]
        
        // Validar token
        claims, err := auth.ValidateToken(tokenString)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token inválido"})
            return
        }
        
        // Añadir userID al contexto
        c.Set("userID", claims.UserID)
        c.Next()
    }
}

7. Documentación con Swagger

7.1 Configurar anotaciones Swagger

// internal/auth/handler.go

// @Summary Registrar nuevo usuario
// @Description Crea una nueva cuenta de usuario
// @Tags auth
// @Accept json
// @Produce json
// @Param input body RegisterRequest true "Datos de registro"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
    // ... implementación existente
}

// @Summary Iniciar sesión
// @Description Autentica un usuario y devuelve un token JWT
// @Tags auth
// @Accept json
// @Produce json
// @Param input body LoginRequest true "Credenciales"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
    // ... implementación existente
}

7.2 Generar documentación

# Generar archivos de documentación
swag init -g cmd/server/main.go

# Esto creará una carpeta docs con los archivos de Swagger

7.3 Configurar ruta Swagger

// cmd/server/main.go
import (
    "github.com/swaggo/gin-swagger"
    "github.com/swaggo/gin-swagger/swaggerFiles"
    _ "github.com/tu-usuario/go-rest-api/docs" // Importar docs generados
)

func main() {
    r := gin.Default()
    
    // Configurar ruta Swagger
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    
    // ... resto de la configuración
}

Después de iniciar el servidor, puedes acceder a la documentación en:

http://localhost:8080/swagger/index.html

8. Despliegue en Linux

8.1 Compilar para Linux

# Compilar para Linux desde cualquier sistema
GOOS=linux GOARCH=amd64 go build -o api cmd/server/main.go

# O compilar directamente en Linux
go build -o api cmd/server/main.go

8.2 Configurar systemd (Opcional)

# /etc/systemd/system/go-api.service
[Unit]
Description=Go REST API
After=network.target

[Service]
User=apiuser
Group=apiuser
WorkingDirectory=/opt/go-api
ExecStart=/opt/go-api/api
Restart=always
Environment="GIN_MODE=release"
Environment="DATABASE_URL=postgres://user:pass@localhost:5432/dbname"

[Install]
WantedBy=multi-user.target

8.3 Comandos para administrar el servicio

# Recargar configuración
sudo systemctl daemon-reload

# Iniciar servicio
sudo systemctl start go-api

# Habilitar inicio automático
sudo systemctl enable go-api

# Ver estado
sudo systemctl status go-api

8.4 Configurar Nginx como proxy inverso

# /etc/nginx/sites-available/go-api
server {
    listen 80;
    server_name api.tudominio.com;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    location /swagger/ {
        proxy_pass http://localhost:8080/swagger/;
    }
}

9. Conclusión

Este manual ha cubierto todos los aspectos esenciales para construir una API REST segura en Go:

Próximos pasos recomendados:

Con esta base, puedes expandir tu API añadiendo más funcionalidades según tus necesidades específicas.