Integración de autenticación y comunicación entre servicios

Video
35 min~11 min lectura
Objetivo de la lección

Hasta ahora, has construido servicios aislados para manejar productos, órdenes y usuarios.

Puntos de control
  • Integración de Autenticación y Comunicación entre Servicios
  • Concepto Clave: El Pasaporte y los Mensajeros en una Ciudad de Servicios
  • Cómo Funciona en la Práctica: Flujo de una Orden Segura
  • Código en Acción: Middleware de Autenticación y Cliente HTTP

Reproductor de video

Integración de Autenticación y Comunicación entre Servicios

En esta lección culminante del proyecto integrador, abordaremos el núcleo de cualquier arquitectura de microservicios madura: la comunicación segura y eficiente entre servicios. Hasta ahora, has construido servicios aislados para manejar productos, órdenes y usuarios. El verdadero poder y la complejidad surgen cuando estos servicios deben colaborar para ejecutar una operación de negocio completa, como procesar una orden, que involucra verificar el inventario, calcular precios, validar al usuario y registrar la transacción. Todo esto debe ocurrir de manera segura, resiliente y trazable, manteniendo la autonomía de cada servicio.

Integraremos un sistema de autenticación basado en JWT (JSON Web Tokens) que actuará como el pasaporte de las solicitudes a través de nuestros microservicios. Luego, implementaremos dos patrones fundamentales de comunicación: comunicación síncrona mediante APIs REST HTTP, ideal para operaciones que requieren una respuesta inmediata, y comunicación asíncrona mediante un broker de mensajes (simulado con canales Go o introduciendo un concepto de cola), perfecta para desacoplar servicios y mejorar la resiliencia. Finalmente, implementaremos el patrón API Gateway como punto único de entrada que maneja la autenticación y enruta las solicitudes.

Concepto Clave: El Pasaporte y los Mensajeros en una Ciudad de Servicios

Imagina nuestros microservicios como edificios especializados en una ciudad: una fábrica (Productos), un banco (Pagos), una oficina de correos (Órdenes) y el registro civil (Usuarios). Para que un ciudadano (cliente) realice una tarea compleja como enviar un paquete certificado, debe interactuar con varios de estos edificios. El JWT es su pasaporte. Lo obtiene una sola vez en el registro civil (login) y lo presenta en cada edificio subsiguiente. Cada edificio puede verificar la autenticidad del pasaporte (firma digital) y leer la información de identidad contenida (payload como user_id, roles) sin necesidad de contactar al registro civil para cada consulta. Esto es autenticación stateless.

Ahora, ¿cómo se comunican los edificios entre sí? Para una consulta rápida y directa, como "¿tienen este producto en stock?", la fábrica envía un mensajero síncrono (HTTP REST) a la bodega. El mensajero espera parado hasta recibir una respuesta "Sí" o "No" antes de continuar. Es simple y directo, pero si la bodega está congestionada, el mensajero (y quien lo envió) queda bloqueado. Para tareas que no requieren respuesta inmediata o que son propensas a fallos, como "notificar al departamento de logística sobre un nuevo envío", se usa un sistema de buzones (message broker). La oficina de correos deja un mensaje en un buzón designado y continúa con su trabajo. El departamento de logística recoge los mensajes de ese buzón a su propio ritmo. Si el buzón está lleno, el mensaje espera allí de forma segura; si el departamento de logística falla, los mensajes se acumulan y se procesan cuando vuelve a funcionar. Esto es comunicación asíncrona.

Cómo Funciona en la Práctica: Flujo de una Orden Segura

Vamos a detallar el flujo paso a paso cuando un usuario autenticado intenta crear una orden. Primero, el cliente envía una solicitud POST a `/api/orders` incluyendo un header `Authorization: Bearer ` y un cuerpo con los `product_ids` y `quantities`. Nuestro API Gateway (o un middleware en el servicio de órdenes) intercepta esta solicitud. Extrae el token JWT, verifica su firma usando una clave secreta compartida (o pública), valida que no haya expirado y extrae el `user_id`. Si todo es válido, adjunta el `user_id` al contexto de la solicitud HTTP y la pasa al controlador de órdenes.

El controlador del servicio de Órdenes recibe la solicitud ya autenticada. Para procesar la orden, necesita verificar el stock y el precio actual de cada producto. Aquí, realiza una serie de llamadas síncronas HTTP al servicio de Productos. Cada llamada debe ser autorizada. Para ello, el servicio de Órdenes actúa como un "servicio-cliente" y utiliza un token de servicio (service account) o reenvía un token de sistema para autenticarse ante el servicio de Productos. Tras recibir las confirmaciones, el servicio de Órdenes persiste la orden en su base de datos con estado "PENDIENTE_DE_PAGO". Inmediatamente después, y sin bloquear la respuesta al usuario, publica un evento asíncrono "OrdenCreada" a un canal de mensajes. Este evento contiene el `order_id`, `user_id` y `total`. El servicio de Notificaciones, suscrito a ese canal, recoge el evento y envía un correo de confirmación al usuario, todo de manera desacoplada.

Código en Acción: Middleware de Autenticación y Cliente HTTP

Implementemos un middleware de autenticación JWT reutilizable para Gorilla/mux y un cliente HTTP robusto con timeout y manejo de errores para la comunicación entre servicios.

Middleware de Autenticación JWT


// pkg/auth/jwt_middleware.go
package auth

import (
    "context"
    "fmt"
    "net/http"
    "strings"
    "github.com/dgrijalva/jwt-go"
)

// Clave secreta (en producción, usaría variables de entorno y una clave segura)
var jwtSecret = []byte("mi_super_secreto_compartido_entre_servicios")

// Claims estructura personalizada para incluir el UserID
type Claims struct {
    UserID string `json:"user_id"`
    jwt.StandardClaims
}

// JWTMiddleware valida el token JWT y extrae los claims
func JWTMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Extraer el token del header Authorization
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, `{"error": "Se requiere token de autorización"}`, http.StatusUnauthorized)
            return
        }

        // El formato esperado es "Bearer "
        bearerToken := strings.Split(authHeader, " ")
        if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
            http.Error(w, `{"error": "Formato de token inválido. Use: Bearer <token>"}`, http.StatusUnauthorized)
            return
        }
        tokenString := bearerToken[1]

        // Parsear y validar el token
        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            // Validar el algoritmo de firma
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("método de firma inesperado: %v", token.Header["alg"])
            }
            return jwtSecret, nil
        })

        if err != nil || !token.Valid {
            http.Error(w, `{"error": "Token inválido o expirado"}`, http.StatusUnauthorized)
            return
        }

        // Token válido. Inyectar el UserID en el contexto de la request
        ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Función de utilidad para generar un token (usada por el servicio de Usuarios/Login)
func GenerateToken(userID string) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)
    claims := &Claims{
        UserID: userID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expirationTime.Unix(),
            Issuer:    "ecommerce-api",
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

Cliente HTTP para Comunicación entre Servicios


// pkg/httpclient/service_client.go
package httpclient

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

// ServiceClient es un cliente HTTP configurado para comunicación entre servicios
type ServiceClient struct {
    baseURL    string
    httpClient *http.Client
    authToken  string // Token para autenticación servicio-a-servicio
}

// NewServiceClient crea una nueva instancia del cliente
func NewServiceClient(baseURL, authToken string) *ServiceClient {
    return &ServiceClient{
        baseURL: baseURL,
        httpClient: &http.Client{
            Timeout: 10 * time.Second, // Timeout crítico para evitar bloqueos
        },
        authToken: authToken,
    }
}

// Get realiza una solicitud GET autenticada a un endpoint
func (c *ServiceClient) Get(endpoint string, v interface{}) error {
    req, err := http.NewRequest("GET", c.baseURL+endpoint, nil)
    if err != nil {
        return fmt.Errorf("creando request GET: %w", err)
    }
    req.Header.Set("Authorization", "Bearer "+c.authToken)
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return fmt.Errorf("ejecutando request GET: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("código de estado %d: %s", resp.StatusCode, string(body))
    }

    return json.NewDecoder(resp.Body).Decode(v)
}

// Post realiza una solicitud POST autenticada
func (c *ServiceClient) Post(endpoint string, body, v interface{}) error {
    jsonBody, err := json.Marshal(body)
    if err != nil {
        return fmt.Errorf("marshalling request body: %w", err)
    }

    req, err := http.NewRequest("POST", c.baseURL+endpoint, bytes.NewBuffer(jsonBody))
    if err != nil {
        return fmt.Errorf("creando request POST: %w", err)
    }
    req.Header.Set("Authorization", "Bearer "+c.authToken)
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return fmt.Errorf("ejecutando request POST: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
        respBody, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("código de estado %d: %s", resp.StatusCode, string(respBody))
    }

    if v != nil {
        return json.NewDecoder(resp.Body).Decode(v)
    }
    return nil
}

Uso en el Servicio de Órdenes


// internal/order/handler.go (fragmento)
package order

import (
    "encoding/json"
    "net/http"
    "ecommerce/pkg/httpclient"
    "github.com/gorilla/mux"
)

type OrderHandler struct {
    service        OrderService
    productClient  *httpclient.ServiceClient // Cliente para el servicio de Productos
}

func NewOrderHandler(s OrderService, productServiceURL, serviceToken string) *OrderHandler {
    return &OrderHandler{
        service:       s,
        productClient: httpclient.NewServiceClient(productServiceURL, serviceToken),
    }
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    // El user_id ya está en el contexto gracias al middleware
    userID := r.Context().Value("user_id").(string)

    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, `{"error": "Cuerpo de solicitud inválido"}`, http.StatusBadRequest)
        return
    }

    // 1. Verificar stock y precios con el servicio de Productos (Comunicación Síncrona)
    var productCheckResponse ProductCheckResponse
    err := h.productClient.Post("/api/products/check-stock", req.Items, &productCheckResponse)
    if err != nil {
        http.Error(w, `{"error": "Error al verificar productos: `+err.Error()+`"}`, http.StatusBadGateway)
        return
    }
    if !productCheckResponse.AllInStock {
        http.Error(w, `{"error": "Uno o más productos no tienen stock suficiente"}`, http.StatusConflict)
        return
    }

    // 2. Crear la orden localmente con el total calculado
    order, err := h.service.CreateOrder(userID, req.Items, productCheckResponse.Total)
    if err != nil {
        http.Error(w, `{"error": "`+err.Error()+`"}`, http.StatusInternalServerError)
        return
    }

    // 3. (Simulación) Publicar evento asíncrono "OrdenCreada"
    // En un caso real, esto iría a Kafka, RabbitMQ, o un canal Go.
    go func() {
        event := OrderCreatedEvent{
            OrderID: order.ID,
            UserID:  userID,
            Total:   order.Total,
        }
        // publicarEventoAsincrono("OrdenCreada", event)
        fmt.Printf("Evento publicado asincrónamente: %+v\n", event)
        // El servicio de Notificaciones escucharía este evento.
    }()

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(order)
}

// En main.go o routes.go
func SetupOrderRoutes(r *mux.Router, handler *OrderHandler) {
    // La ruta está protegida por el middleware JWT
    r.Handle("/api/orders", auth.JWTMiddleware(http.HandlerFunc(handler.CreateOrder))).Methods("POST")
}

Errores Comunes y Cómo Evitarlos

1. Falta de Timeouts en Clientes HTTP: Realizar llamadas entre servicios sin configurar timeouts es una receta para bloqueos en cascada. Si el servicio de Productos se congela, el servicio de Órdenes esperará indefinidamente, agotando sus propios recursos y propagando la falla.

Siempre configura timeouts apropiados (ej., 5-10 segundos) en tu cliente HTTP (`http.Client{Timeout:}`) y considera implementar circuit breakers para fallas repetidas.

2. Token JWT sin Rotación de Claves o Revisión de Expiración: Usar una clave secreta fija por años o no validar la expiración (`ExpiresAt`) del token compromete la seguridad. Un token robado sería válido permanentemente.

Implementa rotación de claves JWT (usando un sistema de keystore) y siempre valida la expiración en el middleware. Usa tokens de vida corta (minutes/horas) y refresh tokens.

3. Acoplamiento por URLs Hardcodeadas: Codificar directamente la URL `http://servicio-productos:8080` en el código del servicio de Órdenes lo hace frágil. Si la ubicación del servicio cambia, debes recompilar y redesplegar.

Utiliza un service discovery (Consul, etcd) o, como mínimo, configura las URLs de los servicios dependientes mediante variables de entorno o un config manager.

4. No Manejar Estados Parciales (Falta de Sagas o Transacciones Distribuidas): En nuestro flujo, ¿qué pasa si la orden se crea pero falla el envío del correo? O peor, ¿si la orden se crea pero la llamada para reducir el stock falla? Puedes quedar con datos inconsistentes.

Para operaciones transaccionales que cruzan servicios, investiga e implementa el patrón Saga (Orquestada o Coreografiada), donde cada paso tiene su compensación para revertir cambios en caso de fallo.

5. Logging sin Correlation IDs: Cuando una solicitud pasa por 4 servicios, depurar un error es una pesadilla si los logs de cada servicio no están vinculados.

Genera un Correlation ID único (como un UUID) en el API Gateway o en el primer servicio, y pásalo en los headers de todas las llamadas HTTP internas (`X-Correlation-ID`). Inclúyelo en cada entrada de log.

Checklist de Dominio

Antes de considerar esta lección completa, asegúrate de poder verificar los siguientes puntos:

  • Puedo explicar la diferencia entre autenticación y autorización en el contexto de microservicios, y cómo JWT facilenta la primera de manera stateless.
  • He implementado un middleware de autenticación JWT que valida la firma, expiración e inyecta claims en el contexto de la petición HTTP.
  • Puedo construir un cliente HTTP robusto para comunicación síncrona entre servicios, incluyendo manejo de timeouts, errores de red y códigos de estado HTTP no exitosos.
  • Comprendo la diferencia fundamental y los casos de uso para comunicación síncrona (HTTP/REST) vs. asíncrona (Message Brokers/Eventos).
  • Sé cómo estructurar y pasar un token de servicio (service-to-service token) en los headers de las solicitudes HTTP internas para autorizar la comunicación.
  • Puedo describir al menos dos problemas de los estados parciales en transacciones distribuidas y mencionar el patrón Saga como una solución potencial.
  • He integrado al menos un servicio con otro de manera segura y resiliente en el proyecto del e-commerce, siguiendo los patrones vistos.
  • Utilizo un Correlation ID para trazar una solicitud a través de los logs de múltiples servicios.
Falar no WhatsApp
De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

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

Integración de autenticación y comunicación ent... | Cursalo