Golang

Artigo 31 — Middlewares: logging, autenticação e CORS Já leu

13 min de leitura

Artigo 31 — Middlewares: logging, autenticação e CORS
Artigo 31 — Middlewares: logging, autenticação e CORS Middlewares: comportamento transversal sem repetição Em qualquer API real, certas preocupações se repetem

Artigo 31 — Middlewares: logging, autenticação e CORS

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs


Middlewares: comportamento transversal sem repetição

Em qualquer API real, certas preocupações se repetem em todas as rotas: toda requisição precisa ser logada, toda requisição protegida precisa ter seu token validado, toda resposta precisa ter os headers CORS corretos. Copiar esse código em cada handler é uma receita para inconsistência e bugs.

Middlewares resolvem isso de forma elegante. São funções que envolvem handlers, formando uma cadeia de responsabilidades onde cada camada adiciona comportamento sem modificar os handlers existentes. O resultado é código que segue o princípio da responsabilidade única — cada peça faz exatamente uma coisa.


A anatomia de um middleware em Go

Um middleware em Go é uma função que recebe um http.Handler e retorna outro http.Handler. Essa assinatura simples é o que permite encadear middlewares de forma uniforme:

type Middleware func(http.Handler) http.Handler

A implementação típica envolve o handler original e executa código antes e/ou depois dele:

func meuMiddleware(prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Código ANTES do handler
        fmt.Println("antes")

        prox.ServeHTTP(w, r) // chama o próximo na cadeia

        // Código DEPOIS do handler
        fmt.Println("depois")
    })
}

ResponseWriter interceptado: capturando status e bytes

Para que middlewares possam registrar o status HTTP e o tamanho da resposta, é necessário envolver o ResponseWriter padrão:

package main

import (
    "net/http"
)

type responseWriter struct {
    http.ResponseWriter
    status       int
    bytesEscritos int
    escrito      bool
}

func novoResponseWriter(w http.ResponseWriter) *responseWriter {
    return &responseWriter{
        ResponseWriter: w,
        status:         http.StatusOK,
    }
}

func (rw *responseWriter) WriteHeader(status int) {
    if !rw.escrito {
        rw.status = status
        rw.escrito = true
        rw.ResponseWriter.WriteHeader(status)
    }
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    if !rw.escrito {
        rw.WriteHeader(http.StatusOK)
    }
    n, err := rw.ResponseWriter.Write(b)
    rw.bytesEscritos += n
    return n, err
}

Middleware de logging estruturado

package main

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

    "github.com/go-chi/chi/v5/middleware"
)

func middlewareLogging(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            inicio := time.Now()
            rw := novoResponseWriter(w)

            // Extrai request ID se existir (compatível com middleware.RequestID do Chi)
            reqID := middleware.GetReqID(r.Context())

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

            duracao := time.Since(inicio)

            // Log após o handler completar
            nivel := slog.LevelInfo
            if rw.status >= 500 {
                nivel = slog.LevelError
            } else if rw.status >= 400 {
                nivel = slog.LevelWarn
            }

            logger.Log(r.Context(), nivel, "requisição HTTP",
                "request_id",  reqID,
                "método",      r.Method,
                "caminho",     r.URL.Path,
                "query",       r.URL.RawQuery,
                "status",      rw.status,
                "bytes",       rw.bytesEscritos,
                "duração_ms",  duracao.Milliseconds(),
                "ip",          r.RemoteAddr,
                "user_agent",  r.UserAgent(),
            )
        })
    }
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))

    mux := http.NewServeMux()
    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })

    handler := middlewareLogging(logger)(mux)
    http.ListenAndServe(":8080", handler)
}

Middleware de autenticação JWT

Na prática, APIs protegidas usam JWT — JSON Web Tokens. O middleware valida o token e injeta os dados do usuário no context:

package main


import (
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "time"
)

// Chave de contexto com tipo próprio para evitar colisões
type chaveContexto string

const ChaveUsuario chaveContexto = "usuario_autenticado"

type Claims struct {
    UserID int    `json:"user_id"`
    Email  string `json:"email"`
    Papel  string `json:"papel"`
    Exp    int64  `json:"exp"`
}

// Validação JWT simplificada — em produção use github.com/golang-jwt/jwt/v5
func validarJWT(tokenStr, segredo string) (*Claims, error) {
    partes := strings.Split(tokenStr, ".")
    if len(partes) != 3 {
        return nil, fmt.Errorf("formato de token inválido")
    }

    // Decodifica o payload (segunda parte)
    payload, err := base64.RawURLEncoding.DecodeString(partes[1])
    if err != nil {
        return nil, fmt.Errorf("payload inválido: %w", err)
    }

    var claims Claims
    if err := json.Unmarshal(payload, &claims); err != nil {
        return nil, fmt.Errorf("claims inválidos: %w", err)
    }

    // Verifica expiração
    if claims.Exp > 0 && time.Now().Unix() > claims.Exp {
        return nil, fmt.Errorf("token expirado")
    }

    return &claims, nil
}

func middlewareJWT(segredo string) func(http.Handler) http.Handler {
    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Extrai o token do header Authorization
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                responderErroAuth(w, "Authorization header obrigatório",
                    http.StatusUnauthorized)
                return
            }

            // Formato esperado: "Bearer <token>"
            partes := strings.SplitN(authHeader, " ", 2)
            if len(partes) != 2 || !strings.EqualFold(partes[0], "Bearer") {
                responderErroAuth(w, "formato inválido: use 'Bearer <token>'",
                    http.StatusUnauthorized)
                return
            }

            claims, err := validarJWT(partes[1], segredo)
            if err != nil {
                responderErroAuth(w, "token inválido: "+err.Error(),
                    http.StatusUnauthorized)
                return
            }

            // Injeta claims no context
            ctx := context.WithValue(r.Context(), ChaveUsuario, claims)
            prox.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Helper para extrair usuário do context
func usuarioDoContext(r *http.Request) (*Claims, bool) {
    claims, ok := r.Context().Value(ChaveUsuario).(*Claims)
    return claims, ok
}

func responderErroAuth(w http.ResponseWriter, msg string, status int) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("WWW-Authenticate", `Bearer realm="api"`)
    w.WriteHeader(status)
    fmt.Fprintf(w, `{"erro":"%s"}`, msg)
}

Middleware de autorização por papel

Construindo sobre o middleware de autenticação, a autorização verifica permissões:

// Exige que o usuário autenticado tenha um dos papéis especificados
func exigirPapel(papeis ...string) func(http.Handler) http.Handler {
    papeisPerm := make(map[string]bool, len(papeis))
    for _, p := range papeis {
        papeisPerm[p] = true
    }

    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims, ok := usuarioDoContext(r)
            if !ok {
                responderErroAuth(w, "autenticação necessária", http.StatusUnauthorized)
                return
            }

            if !papeisPerm[claims.Papel] {
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusForbidden)
                fmt.Fprintf(w, `{"erro":"papel '%s' não tem permissão para esta operação"}`,
                    claims.Papel)
                return
            }

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

Middleware de CORS

CORS — Cross-Origin Resource Sharing — é necessário quando o frontend e o backend estão em origens diferentes. O middleware intercepta requisições preflight e adiciona os headers apropriados:

type ConfigCORS struct {
    OrigensPermitidas []string
    MetodosPermitidos []string
    HeadersPermitidos []string
    HeadersExpostos   []string
    PermitirCredenciais bool
    MaxAge            int // segundos para cache do preflight
}

var ConfigCORSDesenvolvimento = ConfigCORS{
    OrigensPermitidas:   []string{"*"},
    MetodosPermitidos:   []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
    HeadersPermitidos:   []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"},
    MaxAge:              300,
}

var ConfigCORSProducao = ConfigCORS{
    OrigensPermitidas:   []string{"https://app.exemplo.com", "https://admin.exemplo.com"},
    MetodosPermitidos:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    HeadersPermitidos:   []string{"Accept", "Authorization", "Content-Type"},
    HeadersExpostos:     []string{"X-Request-ID", "X-Total-Count"},
    PermitirCredenciais: true,
    MaxAge:              600,
}

func middlewareCORS(config ConfigCORS) func(http.Handler) http.Handler {
    metodos := strings.Join(config.MetodosPermitidos, ", ")
    headers := strings.Join(config.HeadersPermitidos, ", ")
    expostos := strings.Join(config.HeadersExpostos, ", ")
    maxAge := fmt.Sprintf("%d", config.MaxAge)

    origemPermitida := func(origem string) bool {
        for _, o := range config.OrigensPermitidas {
            if o == "*" || o == origem {
                return true
            }
        }
        return false
    }

    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origem := r.Header.Get("Origin")

            if origem != "" && origemPermitida(origem) {
                if len(config.OrigensPermitidas) == 1 &&
                    config.OrigensPermitidas[0] == "*" &&
                    !config.PermitirCredenciais {
                    w.Header().Set("Access-Control-Allow-Origin", "*")
                } else {
                    w.Header().Set("Access-Control-Allow-Origin", origem)
                    w.Header().Set("Vary", "Origin")
                }

                if config.PermitirCredenciais {
                    w.Header().Set("Access-Control-Allow-Credentials", "true")
                }

                if expostos != "" {
                    w.Header().Set("Access-Control-Expose-Headers", expostos)
                }
            }

            // Preflight OPTIONS
            if r.Method == http.MethodOptions {
                w.Header().Set("Access-Control-Allow-Methods", metodos)
                w.Header().Set("Access-Control-Allow-Headers", headers)
                if config.MaxAge > 0 {
                    w.Header().Set("Access-Control-Max-Age", maxAge)
                }
                w.WriteHeader(http.StatusNoContent)
                return
            }

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

Middleware de rate limiting

Protege a API contra abuso limitando o número de requisições por IP:

package main

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

type limitador struct {
    mu       sync.Mutex
    contagem map[string][]time.Time
    limite   int
    janela   time.Duration
}

func novoLimitador(limite int, janela time.Duration) *limitador {
    l := &limitador{
        contagem: make(map[string][]time.Time),
        limite:   limite,
        janela:   janela,
    }

    // Limpa entradas antigas periodicamente
    go func() {
        ticker := time.NewTicker(janela)
        for range ticker.C {
            l.mu.Lock()
            agora := time.Now()
            for ip, tempos := range l.contagem {
                validos := tempos[:0]
                for _, t := range tempos {
                    if agora.Sub(t) < janela {
                        validos = append(validos, t)
                    }
                }
                if len(validos) == 0 {
                    delete(l.contagem, ip)
                } else {
                    l.contagem[ip] = validos
                }
            }
            l.mu.Unlock()
        }
    }()

    return l
}

func (l *limitador) permitir(ip string) (bool, int) {
    l.mu.Lock()
    defer l.mu.Unlock()

    agora := time.Now()
    janela := agora.Add(-l.janela)

    // Filtra apenas requisições dentro da janela
    tempos := l.contagem[ip]
    validos := tempos[:0]
    for _, t := range tempos {
        if t.After(janela) {
            validos = append(validos, t)
        }
    }

    restantes := l.limite - len(validos)
    if restantes <= 0 {
        l.contagem[ip] = validos
        return false, 0
    }

    l.contagem[ip] = append(validos, agora)
    return true, restantes - 1
}

func middlewareRateLimit(limite int, janela time.Duration) func(http.Handler) http.Handler {
    lim := novoLimitador(limite, janela)

    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Extrai IP real considerando proxies
            ip := r.Header.Get("X-Real-IP")
            if ip == "" {
                ip = r.Header.Get("X-Forwarded-For")
            }
            if ip == "" {
                ip = r.RemoteAddr
            }

            permitido, restantes := lim.permitir(ip)

            w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limite))
            w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", restantes))
            w.Header().Set("X-RateLimit-Window", janela.String())

            if !permitido {
                w.Header().Set("Retry-After", janela.String())
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusTooManyRequests)
                fmt.Fprintf(w, `{"erro":"limite de requisições excedido","janela":"%s"}`,
                    janela)
                return
            }

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

Encadeando middlewares: o stack completo

package main

import (
    "encoding/json"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "time"
)

// Encadeador de middlewares — aplica da esquerda para a direita
func encadear(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    segredoJWT := "meu-segredo-super-secreto"

    mux := http.NewServeMux()

    // Rotas públicas
    mux.HandleFunc("GET /saude", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    // Handler protegido
    handlerPerfil := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        claims, _ := usuarioDoContext(r)
        json.NewEncoder(w).Encode(map[string]any{
            "user_id": claims.UserID,
            "email":   claims.Email,
            "papel":   claims.Papel,
        })
    })

    // Handler apenas para admin
    handlerAdmin := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{
            "mensagem": "área administrativa",
        })
    })

    // Aplica middlewares específicos por rota
    mux.Handle("GET /perfil",
        encadear(handlerPerfil,
            middlewareJWT(segredoJWT),
        ),
    )

    mux.Handle("GET /admin",
        encadear(handlerAdmin,
            middlewareJWT(segredoJWT),
            exigirPapel("admin"),
        ),
    )

    // Stack global de middlewares
    handler := encadear(mux,
        middlewareCORS(ConfigCORSDesenvolvimento),
        middlewareRateLimit(100, time.Minute),
        middlewareLogging(logger),
        middlewareRecuperacao(logger),
    )

    servidor := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    fmt.Println("Servidor em :8080")
    servidor.ListenAndServe()
}

// Middleware de recuperação de pânico
func middlewareRecuperacao(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(prox http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if err := recover(); err != nil {
                    logger.Error("pânico recuperado",
                        "erro", fmt.Sprintf("%v", err),
                        "método", r.Method,
                        "caminho", r.URL.Path,
                    )
                    w.Header().Set("Content-Type", "application/json")
                    w.WriteHeader(http.StatusInternalServerError)
                    fmt.Fprint(w, `{"erro":"erro interno do servidor"}`)
                }
            }()
            prox.ServeHTTP(w, r)
        })
    }
}

Testando middlewares

package main

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

func TestMiddlewareCORS(t *testing.T) {
    handler := middlewareCORS(ConfigCORSDesenvolvimento)(
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("ok"))
        }),
    )

    t.Run("preflight OPTIONS", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodOptions, "/", nil)
        req.Header.Set("Origin", "https://app.exemplo.com")
        req.Header.Set("Access-Control-Request-Method", "POST")
        rr := httptest.NewRecorder()

        handler.ServeHTTP(rr, req)

        if rr.Code != http.StatusNoContent {
            t.Errorf("status esperado 204, obtido %d", rr.Code)
        }

        if rr.Header().Get("Access-Control-Allow-Origin") == "" {
            t.Error("header ACAO ausente na resposta preflight")
        }
    })

    t.Run("requisição normal com Origin", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/", nil)
        req.Header.Set("Origin", "https://app.exemplo.com")
        rr := httptest.NewRecorder()

        handler.ServeHTTP(rr, req)

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

func TestMiddlewareRateLimit(t *testing.T) {
    handler := middlewareRateLimit(3, time.Minute)(
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("ok"))
        }),
    )

    // 3 requisições devem passar
    for i := 0; i < 3; i++ {
        req := httptest.NewRequest(http.MethodGet, "/", nil)
        req.RemoteAddr = "192.168.1.1:1234"
        rr := httptest.NewRecorder()
        handler.ServeHTTP(rr, req)

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

    // 4ª deve ser bloqueada
    req := httptest.NewRequest(http.MethodGet, "/", nil)
    req.RemoteAddr = "192.168.1.1:1234"
    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)

    if rr.Code != http.StatusTooManyRequests {
        t.Errorf("esperado 429, obtido %d", rr.Code)
    }
}

Resumo do que foi coberto

Este artigo apresentou middlewares em Go de forma completa: a anatomia e assinatura padrão, o responseWriter interceptado para capturar status e bytes, middleware de logging estruturado com slog, autenticação JWT com injeção de claims no context, autorização por papel, CORS com suporte a preflight e configurações de produção e desenvolvimento, rate limiting por IP com janela deslizante, encadeamento de middlewares e testes com httptest. Com este artigo o Módulo 5 está completo.


Referências e leituras complementares

  • golang-jwt/jwt — Biblioteca padrão da comunidade para JWT em Go. https://github.com/golang-jwt/jwt

  • go-chi/chi/middleware — Middlewares prontos e bem testados do ecossistema Chi. https://pkg.go.dev/github.com/go-chi/chi/v5/middleware

  • MDN — Cross-Origin Resource Sharing — Referência completa sobre CORS. https://developer.mozilla.org/pt-BR/docs/Web/HTTP/CORS

  • rs/cors — Biblioteca dedicada a CORS com suporte completo à especificação. https://github.com/rs/cors

  • golang.org/x/time/rate — Implementação oficial de rate limiter com token bucket. https://pkg.go.dev/golang.org/x/time/rate

  • Go by Example: HTTP Middleware — Exemplos práticos de middleware. https://gobyexample.com/middleware


Próximo artigo: Artigo 32 — Introdução ao database/sql e drivers em Go**


you asked

Sim


claude response

Comentários

Mais em Golang

Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy
Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy

Artigo 47 &mdash; Design Patterns em Go: Repository, Factory e Strategy Curs...

Ponteiros: conceito, uso e quando evitar
Ponteiros: conceito, uso e quando evitar

Ponteiros s&atilde;o um dos t&oacute;picos que mais intimidam iniciantes vind...

Go com PostgreSQL: recursos avançados e o driver pgx
Go com PostgreSQL: recursos avançados e o driver pgx

PostgreSQL é frequentemente descrito como o banco de dados relacional mais co...