Go Admin

Middleware de Autenticação, Logging e Rate Limiting em Go na Prática Já leu

Introdução: Por que Middleware Importa em Go Middleware é um padrão fundamental em desenvolvimento web moderno. Pense nele como uma série de filtros que toda requisição HTTP passa antes de chegar ao seu handler final. Em Go, diferentemente de frameworks pesados como Express.js, implementamos middleware de forma elegante e eficiente usando o padrão de composição de funções. Neste artigo, vamos explorar três middlewares críticos: autenticação (validação de identidade), logging (rastreamento de requisições) e rate limiting (controle de acesso). Esses componentes são a espinha dorsal de qualquer API production-ready. Vou te ensinar não apenas como implementá-los, mas por que funcionam dessa forma e como combiná-los em uma pipeline real. Middleware de Autenticação com JWT Entendendo o Fluxo de Autenticação Autenticação responde a uma pergunta simples: "Quem é você?". Em APIs modernas, usamos JWT (JSON Web Tokens) porque são stateless — o servidor não precisa manter sessões em memória. Um JWT é um token criptografado que contém informações do usuário e uma

Introdução: Por que Middleware Importa em Go

Middleware é um padrão fundamental em desenvolvimento web moderno. Pense nele como uma série de filtros que toda requisição HTTP passa antes de chegar ao seu handler final. Em Go, diferentemente de frameworks pesados como Express.js, implementamos middleware de forma elegante e eficiente usando o padrão de composição de funções.

Neste artigo, vamos explorar três middlewares críticos: autenticação (validação de identidade), logging (rastreamento de requisições) e rate limiting (controle de acesso). Esses componentes são a espinha dorsal de qualquer API production-ready. Vou te ensinar não apenas como implementá-los, mas por que funcionam dessa forma e como combiná-los em uma pipeline real.

Middleware de Autenticação com JWT

Entendendo o Fluxo de Autenticação

Autenticação responde a uma pergunta simples: "Quem é você?". Em APIs modernas, usamos JWT (JSON Web Tokens) porque são stateless — o servidor não precisa manter sessões em memória. Um JWT é um token criptografado que contém informações do usuário e uma assinatura que prova sua validade.

O fluxo é: cliente envia token no header Authorization, middleware valida a assinatura e extrai os dados do usuário, permitindo que handlers downstream acessem essas informações. Se o token for inválido ou expirado, rejeitamos a requisição imediatamente.

Implementação Prática de Autenticação

package main

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

// Claims define a estrutura das informações no JWT
type Claims struct {
    UserID   string `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

var jwtSecret = []byte("sua-chave-secreta-super-segura-aqui")

// GenerateToken cria um JWT válido por 24 horas
func GenerateToken(userID, username, role string) (string, error) {
    claims := &Claims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

// ValidateToken extrai e valida um JWT
func ValidateToken(tokenString string) (*Claims, error) {
    claims := &Claims{}
    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, errors.New("metodo de assinatura inesperado")
        }
        return jwtSecret, nil
    })

    if err != nil || !token.Valid {
        return nil, errors.New("token inválido ou expirado")
    }

    return claims, nil
}

// AuthMiddleware valida o JWT antes de chamar o próximo handler
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Token não fornecido", http.StatusUnauthorized)
            return
        }

        // Esperamos formato: "Bearer <token>"
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Formato de token inválido", http.StatusUnauthorized)
            return
        }

        claims, err := ValidateToken(parts[1])
        if err != nil {
            http.Error(w, fmt.Sprintf("Falha na validação: %v", err), http.StatusUnauthorized)
            return
        }

        // Armazena claims no contexto para acesso posterior
        ctx := context.WithValue(r.Context(), "claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Exemplo de handler protegido
func protectedHandler(w http.ResponseWriter, r *http.Request) {
    claims := r.Context().Value("claims").(*Claims)
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"message":"Bem-vindo %s","role":"%s"}`, claims.Username, claims.Role)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    token, _ := GenerateToken("user123", "joao", "admin")
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"token":"%s"}`, token)
}

O que está acontecendo aqui: O middleware intercepta toda requisição, extrai o token do header Authorization, valida a assinatura usando a chave secreta, e se tudo estiver correto, armazena as informações do usuário (claims) no contexto da requisição. Handlers downstream podem acessar essas informações sem validar novamente.

Middleware de Logging Estruturado

Por Que Logging Estruturado?

Logging simples com fmt.Println não escala. Em produção, você precisa saber exatamente quando uma requisição chegou, quanto tempo levou, qual status HTTP retornou, quem fez a requisição e se houve erro. Logging estruturado significa armazenar essas informações em formato consistente (JSON) para análise posterior.

Um middleware de logging é perfeito para isso: ele consegue medir o tempo total da requisição e capturar o status final sem precisar modificar seus handlers. Usaremos o pacote log/slog do Go (disponível desde Go 1.21), que é a abordagem padrão moderna.

Implementação com Slog

package main

import (
    "log/slog"
    "net/http"
    "time"
)

// responseWriter wrapper captura o status HTTP escrito
type responseWriter struct {
    http.ResponseWriter
    statusCode int
    written    bool
}

func (rw *responseWriter) WriteHeader(code int) {
    if !rw.written {
        rw.statusCode = code
        rw.written = true
    }
    rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    if !rw.written {
        rw.statusCode = http.StatusOK
        rw.written = true
    }
    return rw.ResponseWriter.Write(b)
}

// LoggingMiddleware registra detalhes de cada requisição
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Envolve o ResponseWriter original
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        // Executa o handler
        next.ServeHTTP(rw, r)

        // Registra informações
        duration := time.Since(start).Milliseconds()
        slog.Info("requisição processada",
            slog.String("método", r.Method),
            slog.String("caminho", r.RequestURI),
            slog.Int("status", rw.statusCode),
            slog.Int64("duração_ms", duration),
            slog.String("ip_cliente", r.RemoteAddr),
            slog.String("user_agent", r.Header.Get("User-Agent")),
        )
    })
}

Por que wrappear o ResponseWriter? Por padrão, você não sabe qual status HTTP foi enviado sem interceptar a chamada WriteHeader(). Envolvemos o writer original e capturamos esse valor, permitindo logar informações completas sobre a resposta.

Middleware de Rate Limiting

Estratégias de Rate Limiting

Rate limiting protege seu servidor contra abuso e sobrecarga. Existem várias estratégias: fixos (máximo de requisições por hora), token bucket (permite "bursts" controlados) e sliding window (janela móvel mais precisa). Implementaremos token bucket porque é elegante, justo e eficiente.

A ideia é simples: cada cliente tem um "balde" que se enche com tokens a uma taxa fixa (ex: 100 tokens/minuto). Cada requisição custa 1 token. Se o balde esvaziar, o cliente precisa esperar. Isso permite picos ocasionais mas protege contra abuso sustentado.

Implementação com Token Bucket

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

// ClientBucket representa o estado de rate limit de um cliente
type ClientBucket struct {
    tokens    float64
    lastRefill time.Time
    mu        sync.Mutex
}

// TokenBucketLimiter gerencia buckets por IP/usuário
type TokenBucketLimiter struct {
    maxTokens   float64
    refillRate  float64 // tokens por segundo
    buckets     map[string]*ClientBucket
    mu          sync.RWMutex
    cleanupTime time.Duration
}

// NewTokenBucketLimiter cria um novo limitador
// maxTokens: capacidade máxima do balde
// requestsPerSecond: taxa de refill
func NewTokenBucketLimiter(maxTokens float64, requestsPerSecond float64) *TokenBucketLimiter {
    limiter := &TokenBucketLimiter{
        maxTokens:   maxTokens,
        refillRate:  requestsPerSecond,
        buckets:     make(map[string]*ClientBucket),
        cleanupTime: 5 * time.Minute,
    }

    // Limpa buckets inativos periodicamente
    go func() {
        ticker := time.NewTicker(1 * time.Minute)
        for range ticker.C {
            limiter.cleanup()
        }
    }()

    return limiter
}

// getBucket retorna ou cria o bucket de um cliente
func (t *TokenBucketLimiter) getBucket(clientID string) *ClientBucket {
    t.mu.Lock()
    defer t.mu.Unlock()

    if bucket, exists := t.buckets[clientID]; exists {
        return bucket
    }

    bucket := &ClientBucket{
        tokens:    t.maxTokens,
        lastRefill: time.Now(),
    }
    t.buckets[clientID] = bucket
    return bucket
}

// Allow verifica se uma requisição é permitida
func (t *TokenBucketLimiter) Allow(clientID string) bool {
    bucket := t.getBucket(clientID)
    bucket.mu.Lock()
    defer bucket.mu.Unlock()

    // Calcula quantos tokens devem ser adicionados
    now := time.Now()
    elapsed := now.Sub(bucket.lastRefill).Seconds()
    tokensToAdd := elapsed * t.refillRate

    bucket.tokens = min(bucket.tokens+tokensToAdd, t.maxTokens)
    bucket.lastRefill = now

    // Tenta consumir um token
    if bucket.tokens >= 1 {
        bucket.tokens--
        return true
    }

    return false
}

// cleanup remove buckets não usados há muito tempo
func (t *TokenBucketLimiter) cleanup() {
    t.mu.Lock()
    defer t.mu.Unlock()

    now := time.Now()
    for clientID, bucket := range t.buckets {
        bucket.mu.Lock()
        if now.Sub(bucket.lastRefill) > t.cleanupTime {
            delete(t.buckets, clientID)
        }
        bucket.mu.Unlock()
    }
}

func min(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

// RateLimitMiddleware aplica limitação de requisições
func RateLimitMiddleware(limiter *TokenBucketLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Usa IP do cliente como identificador (na prática, use userID se autenticado)
            clientID := r.RemoteAddr

            // Se autenticado, prefere usar o user ID
            if claims, ok := r.Context().Value("claims").(*Claims); ok {
                clientID = claims.UserID
            }

            if !limiter.Allow(clientID) {
                w.Header().Set("Retry-After", "60")
                http.Error(w, "Muitas requisições, tente novamente mais tarde", http.StatusTooManyRequests)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Detalhe importante: Ao validar autenticação antes de aplicar rate limiting, você pode usar o userID em vez do IP. Isso é muito mais justo: um cliente legítimo atrás de um NAT compartilhado não é punido pelo abuso de outro cliente.

Composição de Middlewares em Uma Pipeline Real

Ordem Importa

A ordem em que você aplica middlewares é crítica. Logging deve ser outermost (registra tudo), depois rate limiting (protege o servidor antes de processing pesado), depois autenticação (valida identidade), e por último seus handlers. A cadeia funciona de fora para dentro.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Cria os middlewares
    limiter := NewTokenBucketLimiter(100, 10) // 100 tokens, 10/segundo

    // Define as rotas
    mux := http.NewServeMux()

    // Rota pública (apenas logging)
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "OK")
    })

    // Rota de login (logging + rate limit)
    loginRoute := LoggingMiddleware(
        RateLimitMiddleware(limiter)(
            http.HandlerFunc(loginHandler),
        ),
    )
    mux.Handle("/login", loginRoute)

    // Rota protegida (logging + rate limit + autenticação)
    protectedRoute := LoggingMiddleware(
        RateLimitMiddleware(limiter)(
            AuthMiddleware(
                http.HandlerFunc(protectedHandler),
            ),
        ),
    )
    mux.Handle("/protected", protectedRoute)

    // Alternativamente, função helper para evitar aninhamento
    protectedRoute2 := Chain(
        http.HandlerFunc(protectedHandler),
        AuthMiddleware,
        RateLimitMiddleware(limiter),
        LoggingMiddleware,
    )
    mux.Handle("/api/profile", protectedRoute2)

    // Inicia servidor
    fmt.Println("Servidor rodando em :8080")
    http.ListenAndServe(":8080", mux)
}

// Chain aplicar múltiplos middlewares em ordem
func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    // Inverte a ordem para aplicar do mais interno ao mais externo
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

O padrão de composição: Cada middleware é uma função que recebe um http.Handler e retorna outro http.Handler. Essa é a beleza da programação funcional em Go — você combina pequenas funções especializadas para criar comportamentos complexos sem frameworks pesados.

Testando Middlewares

Testes Unitários Práticos

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestAuthMiddlewareSuccess(t *testing.T) {
    token, _ := GenerateToken("user1", "test", "admin")

    handler := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        claims := r.Context().Value("claims").(*Claims)
        if claims.UserID != "user1" {
            t.Errorf("UserID esperado: user1, obtido: %s", claims.UserID)
        }
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "OK")
    }))

    req := httptest.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
    rw := httptest.NewRecorder()

    handler.ServeHTTP(rw, req)

    if rw.Code != http.StatusOK {
        t.Errorf("Status esperado: 200, obtido: %d", rw.Code)
    }
}

func TestAuthMiddlewareNoToken(t *testing.T) {
    handler := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))

    req := httptest.NewRequest("GET", "/protected", nil)
    rw := httptest.NewRecorder()

    handler.ServeHTTP(rw, req)

    if rw.Code != http.StatusUnauthorized {
        t.Errorf("Status esperado: 401, obtido: %d", rw.Code)
    }
}

func TestRateLimitingBlocks(t *testing.T) {
    limiter := NewTokenBucketLimiter(2, 1) // 2 tokens, 1/segundo

    handler := RateLimitMiddleware(limiter)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }))

    clientID := "test-client"

    // Primeiras 2 requisições devem passar
    for i := 0; i < 2; i++ {
        req := httptest.NewRequest("GET", "/", nil)
        req.RemoteAddr = clientID
        rw := httptest.NewRecorder()
        handler.ServeHTTP(rw, req)

        if rw.Code != http.StatusOK {
            t.Errorf("Requisição %d: esperado 200, obtido %d", i+1, rw.Code)
        }
    }

    // Terceira deve ser bloqueada
    req := httptest.NewRequest("GET", "/", nil)
    req.RemoteAddr = clientID
    rw := httptest.NewRecorder()
    handler.ServeHTTP(rw, req)

    if rw.Code != http.StatusTooManyRequests {
        t.Errorf("Requisição bloqueada: esperado 429, obtido %d", rw.Code)
    }
}

Teste sempre seus middlewares isoladamente: Use httptest.NewRequest() e httptest.NewRecorder() para simular requisições HTTP sem depender de um servidor real. Isso torna seus testes rápidos e confiáveis.

Conclusão

Você aprendeu três padrões fundamentais que qualquer API profissional precisa: autenticação stateless via JWT (validar identidade de forma segura e escalável), logging estruturado (rastrear requisições com contexto completo para debugging), e rate limiting com token bucket (proteger servidores contra abuso sem ser injusto com clientes legítimos).

A chave em Go é entender que middleware é simplesmente composição de funções — não há magia. Cada middleware pode ser testado isoladamente, combinado em qualquer ordem, e reutilizado em múltiplas rotas. Isso torna seu código mantível, testável e escalável em produção.

Referências


Artigos relacionados