Implementación de middleware para logging y seguridad

Video
25 min~10 min lectura

Reproductor de video

Introducción al Middleware en Go: Más Allá del Enrutamiento Básico

Cuando construimos APIs de alto rendimiento con Go, el enrutador es solo el punto de entrada. La verdadera robustez, seguridad y observabilidad de nuestro microservicio se construye en las capas intermedias que procesan cada solicitud y respuesta. Estas capas son el middleware. Imagina una cadena de montaje en una fábrica de automóviles. El chasis (la solicitud HTTP) entra en la línea, y antes de convertirse en un coche terminado (la respuesta), pasa por estaciones que lo pintan (logging), instalan airbags (seguridad), prueban los frenos (validación) y pulen la carrocería (compresión). Cada estación es un middleware: una función que recibe un http.Handler, realiza una operación, y pasa el control al siguiente handler en la cadena.

En esta lección, nos centraremos en dos pilares fundamentales para cualquier API en producción: el logging estructurado y las capas básicas de seguridad. El logging no es solo imprimir líneas en consola; es capturar un contexto rico y uniforme de cada interacción para poder diagnosticar problemas, entender el tráfico y cumplir con auditorías. La seguridad, por su parte, comienza con mecanismos de defensa perimetral como el control de cabeceras CORS y la limitación de tasa de peticiones (rate limiting), que protegen nuestros recursos de accesos malintencionados o accidentales. Utilizaremos el potente router gorilla/mux como base, pero los conceptos son aplicables a cualquier router o al net/http estándar.

La belleza del patrón middleware en Go radica en su simplicidad conceptual y su poder compositivo. Al dominar esta técnica, podrás ensamblar comportamientos complejos a partir de piezas simples, reutilizables y fáciles de testear, manteniendo tu handler final limpio y enfocado únicamente en la lógica de negocio. Este es un sello distintivo de las APIs Go bien diseñadas.

Concepto Clave: La Cadena de Responsabilidad HTTP

Para entender el middleware, debemos internalizar el patrón de diseño Cadena de Responsabilidad (Chain of Responsibility). En el mundo de las APIs, cada solicitud HTTP viaja a través de una cadena de handlers. Cada eslabón de esta cadena tiene la oportunidad de: 1) Realizar una acción antes de pasar la solicitud al siguiente (por ejemplo, registrar la hora de inicio, autenticar al usuario). 2) Realizar una acción después de que el siguiente handler haya procesado la solicitud (por ejemplo, registrar el tiempo total de procesamiento, encriptar la respuesta). 3) Decidir NO pasar la solicitud al siguiente handler y responder de inmediato (por ejemplo, rechazar una petición no autenticada o con formato erróneo).

Una analogía del mundo real sería un control de acceso a un edificio seguro. Primero, un guardia (middleware de logging) registra en su bitácora tu hora de entrada y documento de identidad. Luego, un escáner de seguridad (middleware de validación) verifica que no lleves objetos prohibidos. Después, un lector de tarjetas (middleware de autenticación) comprueba que tu credencial esté activa. Solo si pasas todas estas estaciones, llegas a la recepcionista (tu handler de negocio) que puede atender tu solicitud específica. En el camino de salida, el guardia podría registrar nuevamente tu hora de salida. Cada capa es independiente y tiene una responsabilidad única y bien definida.

Técnicamente, en Go, esto se implementa creando un tipo que satisfaga la interfaz http.Handler (teniendo un método ServeHTTP(http.ResponseWriter, *http.Request)) y que, en su implementación, "envuelva" a otro handler. La función middleware típica toma un http.Handler como argumento y devuelve un nuevo http.Handler. El nuevo handler, cuando es ejecutado, ejecuta su lógica personalizada y luego llama al ServeHTTP del handler original que recibió como parámetro.

Tip del Instructor: Piensa en el middleware como una cebolla (o un ogro). Tu handler de negocio está en el centro. Cada capa de middleware es una capa de la cebolla. La solicitud atraviesa las capas hacia el centro, y la respuesta viaja desde el centro a través de las mismas capas hacia el exterior. El orden en que apilas el middleware es, por tanto, crítico.

Cómo Funciona en la Práctica: Construyendo un Middleware de Logging Estructurado

Vamos a construir un middleware de logging paso a paso. Un log útil en producción debe ser estructurado (por ejemplo, en JSON) para que sistemas como ELK o Grafana Loki puedan indexarlo y analizarlo fácilmente. Debe capturar métricas clave: identificador de petición, método HTTP, ruta, dirección IP remota, código de estado de respuesta y tiempo de ejecución. El primer paso es definir la firma de nuestra función middleware. Será una función que reciba un http.Handler y devuelva un http.Handler. Dentro del handler que retornamos, capturaremos el momento de inicio, dejaremos que el handler interno procese la solicitud, y luego registraremos los datos junto con la duración.

Un detalle crucial es que el http.ResponseWriter estándar no nos permite leer el código de estado HTTP que nuestro handler final haya establecido (por ejemplo, 200, 404, 500). Para solucionar esto, necesitamos crear un ResponseWriter wrapper. Este es un tipo personalizado que embebe el http.ResponseWriter original, pero sobrescribe el método WriteHeader para capturar el código de estado antes de pasarlo al writer original. Este patrón es común y necesario para muchos middlewares que necesitan inspeccionar la respuesta.

Finalmente, para emitir los logs, utilizaremos un logger estructurado como el de la biblioteca estándar log/slog (disponible desde Go 1.21) o una popular como zerolog o logrus. En nuestro ejemplo usaremos slog por ser parte de la stdlib. El middleware no debería hacer fmt.Println. Configuraremos el logger para que salga en formato JSON y lo inyectaremos en el contexto de la petición o lo usaremos como una variable de paquete, dependiendo de la arquitectura de la aplicación.


// Ejemplo de un wrapper para ResponseWriter
type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.statusCode = code
    lrw.ResponseWriter.WriteHeader(code)
}

// Middleware de logging estructurado con slog
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Crear nuestro wrapper para capturar el status code
        lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        // Pasar el control al siguiente handler (que puede ser otro middleware o el handler final)
        next.ServeHTTP(lrw, r)

        // Calcular duración
        duration := time.Since(start)

        // Emitir log estructurado
        slog.Info("petición HTTP procesada",
            "method", r.Method,
            "path", r.URL.Path,
            "remote_addr", r.RemoteAddr,
            "status", lrw.statusCode,
            "duration_ms", duration.Milliseconds(),
            "user_agent", r.UserAgent(),
        )
    })
}

Código en Acción: API Segura con Middleware de CORS y Rate Limiting

A continuación, presentamos un ejemplo completo y funcional de un microservicio que integra múltiples capas de middleware usando gorilla/mux. Crearemos un router, aplicaremos un middleware de logging, luego uno de CORS (usando la madura biblioteca rs/cors) y finalmente uno de rate limiting básico. También añadiremos un middleware de recuperación de pánicos (panic recovery) para asegurar que nuestro servidor no se caiga por un error inesperado en un handler, sino que responda con un error 500 controlado. Este es un stack de middleware típico para una API REST en producción.

El middleware de CORS configura las cabeceras HTTP que permiten a aplicaciones web en dominios diferentes al de nuestra API realizar peticiones de forma segura. Es una capa de seguridad esencial. El rate limiting protege contra ataques de fuerza bruta o abuso de la API limitando el número de peticiones que un cliente puede hacer en un período de tiempo. Nuestra implementación será simple por IP, pero en sistemas distribuidos necesitarías usar algo como Redis. El middleware de recuperación atrapa cualquier panic() que ocurra en la cadena de handlers, lo registra como error y devuelve una respuesta 500 Internal Server Error, manteniendo el servicio estable.

Observa cómo los middlewares se aplican al router usando router.Use(...). El orden de aplicación es el orden de ejecución para la fase de "ida" (antes del handler). El middleware de recuperación debe ir primero, para atrapar pánicos en todos los middlewares subsiguientes. El logging suele ir después, para poder cronometrar toda la cadena, incluido el tiempo de los otros middlewares. CORS y rate limiting son políticas de acceso que deben evaluarse pronto, pero después del logging básico.


package main

import (
    "context"
    "net/http"
    "sync"
    "time"
    "log/slog"
    "github.com/gorilla/mux"
    "github.com/rs/cors"
)

// 1. Middleware de Recuperación de Pánico
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                slog.Error("panic recuperado en middleware", "error", err, "path", r.URL.Path)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// 2. Middleware de Rate Limiting Simple (por IP)
type rateLimiter struct {
    mu      sync.Mutex
    visits  map[string]time.Time
    limit   time.Duration // e.g., 1 request per second = time.Second
}

func NewRateLimiter(limit time.Duration) *rateLimiter {
    return &rateLimiter{
        visits: make(map[string]time.Time),
        limit:  limit,
    }
}

func (rl *rateLimiter) RateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr // En producción, usa X-Forwarded-For o similar
        rl.mu.Lock()
        last, exists := rl.visits[ip]
        now := time.Now()
        if exists && now.Sub(last) < rl.limit {
            rl.mu.Unlock()
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        rl.visits[ip] = now
        rl.mu.Unlock()
        // Limpieza periódica del mapa (en un goroutine separado en un caso real)
        next.ServeHTTP(w, r)
    })
}

func main() {
    // Configurar logger estructurado
    slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

    router := mux.NewRouter()

    // Inicializar rate limiter (permite 1 petición por segundo por IP)
    limiter := NewRateLimiter(time.Second)

    // APLICAR MIDDLEWARE EN ORDEN (de afuera hacia adentro)
    router.Use(RecoveryMiddleware)           // Primero: recuperar panics
    router.Use(LoggingMiddleware)            // Segundo: loggear todo
    router.Use(limiter.RateLimitMiddleware)  // Tercero: aplicar límite de tasa

    // Configurar y aplicar middleware CORS
    corsHandler := cors.New(cors.Options{
        AllowedOrigins:   []string{"https://dominio-permitido.com", "http://localhost:3000"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
        AllowCredentials: true,
        MaxAge:           86400, // 24 horas en segundos
    })
    // cors.Default() configura un CORS permisivo para desarrollo. NO usarlo en producción.

    // El handler CORS también es un middleware. Lo envolvemos.
    handlerWithCORS := corsHandler.Handler(router)

    // Definir ruta de ejemplo
    router.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status": "ok"}`))
    }).Methods("GET")

    router.HandleFunc("/api/v1/panic-test", func(w http.ResponseWriter, r *http.Request) {
        panic("esto es una prueba de panic controlado")
    })

    slog.Info("Servidor iniciado en el puerto 8080")
    if err := http.ListenAndServe(":8080", handlerWithCORS); err != nil {
        slog.Error("error al iniciar el servidor", "error", err)
    }
}

Errores Comunes y Cómo Evitarlos

1. No capturar el código de estado HTTP en el middleware de logging. Usar el http.ResponseWriter original directamente hace imposible saber si el handler respondió con 200, 404 o 500. Siempre crea un wrapper que sobrescriba WriteHeader para capturar este dato esencial para el diagnóstico.

2. Orden incorrecto de los middlewares. Aplicar el middleware de CORS después del handler que responde hará que las cabeceras CORS no se incluyan. Poner el rate limiting después de una operación de base de datos pesada anula su propósito de proteger recursos. El orden típico recomendado es: 1) Recovery, 2) Logging/ Tracing, 3) Seguridad (CORS, Rate Limit), 4) Autenticación/Autorización, 5) Handlers de negocio.

3. Logs no estructurados y con información sensible. Evita usar fmt.Printf o log.Println. Usa un logger estructurado como slog, zerolog o logrus. Nunca registres cuerpos de petición completos, contraseñas, tokens o datos personales (PII) en los logs. Configura niveles de log (INFO, ERROR, DEBUG) apropiadamente.

4. Rate limiting ingenuo en un entorno distribuido. Un contador en memoria por IP, como nuestro ejemplo, no funciona si tienes múltiples instancias del servidor detrás de un balanceador de carga. Un atacante podría enviar peticiones a diferentes instancias y evadir el límite. Para producción, utiliza un almacén centralizado como Redis con algoritmos como el "token bucket" o "fixed window", implementados con paquetes como go-rate-limiter.

5. Olvidar el manejo de pánicos (panic recovery). Cualquier panic() no recuperada en un handler hará que todo el proceso del servidor Go se cierre, causando downtime. Siempre incluye un middleware de recuperación al inicio de la cadena. Este middleware debe registrar el error con tanto detalle como sea posible para depuración, y responder con un error 500 genérico sin filtrar información interna al cliente.

Checklist de Dominio

  • Puedo explicar el patrón Cadena de Responsabilidad y cómo se aplica al middleware HTTP.
  • Sé crear una función middleware en Go que reciba y devuelva un http.Handler.
  • He implementado un wrapper de http.ResponseWriter para capturar el código de estado HTTP.
  • Puedo configurar y aplicar middlewares en un router de gorilla/mux usando router.Use() y comprendo la importancia del orden.
  • He integrado al menos un middleware de seguridad (CORS o Rate Limiting) en una API real.
  • Utilizo logging estructurado (con slog, zerolog, etc.) en mis middlewares, evitando salidas de texto plano.
  • Mi API incluye un middleware de recuperación de pánicos que evita la caída del servidor por errores no controlados.
  • Puedo identificar y justificar el orden de ejecución de un stack típico de middlewares para una API en producción.
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión