Middlewares: logging, autenticação e CORS

Middlewares: logging, autenticação e CORS

Middlewares em Go: responseWriter interceptado, logging com slog, JWT com assinatura HS256 verificada de fato, autorização por papel, CORS com preflight, rate limit que não confia em cabeçalhos do cliente nem na porta, Retry-After em segundos e a ordem da cadeia.
Golang

• • 19 min de leitura

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"
    "crypto/hmac"
    "crypto/sha256"
    "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 HS256 mínima, com verificação de assinatura.
// Em produção, prefira github.com/golang-jwt/jwt/v5, que trata todos os casos da especificação.
func validarJWT(tokenStr, segredo string) (*Claims, error) {
    partes := strings.Split(tokenStr, ".")
    if len(partes) != 3 {
        return nil, fmt.Errorf("formato de token inválido")
    }

    // 1. O cabeçalho precisa declarar o algoritmo esperado — nunca aceite "none"
    cabecalho, err := base64.RawURLEncoding.DecodeString(partes[0])
    if err != nil {
        return nil, fmt.Errorf("cabeçalho inválido: %w", err)
    }
    var h struct {
        Alg string `json:"alg"`
    }
    if err := json.Unmarshal(cabecalho, &h); err != nil || h.Alg != "HS256" {
        return nil, fmt.Errorf("algoritmo não aceito")
    }

    // 2. A assinatura precisa bater com HMAC-SHA256(cabeçalho.payload, segredo)
    mac := hmac.New(sha256.New, []byte(segredo))
    mac.Write([]byte(partes[0] + "." + partes[1]))
    esperada := mac.Sum(nil)
    recebida, err := base64.RawURLEncoding.DecodeString(partes[2])
    if err != nil || !hmac.Equal(recebida, esperada) {
        return nil, fmt.Errorf("assinatura inválida")
    }

    // 3. Só depois de verificada a assinatura o payload merece confiança
    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"
    "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) {
            // IP do cliente: o endereço da conexão, sem a porta.
            // X-Forwarded-For e X-Real-IP só merecem confiança quando o servidor
            // fica atrás de um proxy conhecido, que sobrescreve esses cabeçalhos.
            ip, _, err := net.SplitHostPort(r.RemoteAddr)
            if err != nil {
                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", fmt.Sprintf("%d", int(janela.Seconds()))) // em segundos
                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"
    "time"
)

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)
    }
}

Middlewares são a forma de o Go tratar o que atravessa todas as rotas — log, autenticação, CORS, limites, recuperação de pânico — com funções que embrulham handlers. A assinatura simples esconde decisões de segurança: um token só vale depois de a assinatura ser verificada, um IP só vale quando vem da conexão ou de um proxy confiável, uma mensagem de erro não deve contar ao cliente como a validação funciona. E a ordem da cadeia é parte da lógica: ela decide o que é registrado, o que é barrado antes e quem consegue recuperar um pânico de quem.

Fontes e leituras recomendadas

Exercícios

Exercício 1

A primeira versão do validarJWT deste artigo recebia o parâmetro segredo, decodificava o payload, conferia a expiração e devolvia as claims. Um revisor observa que o parâmetro nunca era usado. Que ataque isso permitia, e o que a versão atual verifica antes de confiar no conteúdo do token?

Ver resposta

✓ Resposta: Permitia forjar qualquer identidade. Um JWT é só três trechos em base64 separados por pontos, e qualquer pessoa consegue montar um com o payload que quiser. A única coisa que impede isso é a assinatura, que só quem tem o segredo consegue calcular. Sem verificá-la, o middleware aceitava tokens inventados: num teste com o código original, um token com "papel":"admin", cabeçalho alg: none e o texto "assinatura-qualquer" no lugar da assinatura recebeu 200 na área administrativa. A versão atual faz três coisas, nesta ordem. Primeiro, exige que o cabeçalho declare HS256, recusando none e qualquer outro algoritmo. Depois, calcula o HMAC-SHA256 de cabeçalho.payload com o segredo e compara com a assinatura recebida usando hmac.Equal, que leva o mesmo tempo independentemente de onde os bytes diferem. Só então lê as claims. Com essa versão, o token forjado passou a receber 401, e um token assinado com o segredo certo, 200. A lição vai além do JWT: dado vindo do cliente só vira fato depois de verificado, e um parâmetro de segurança não usado é sempre sinal de alerta.

Exercício 2

O limitador de taxa original usava X-Real-IP, depois X-Forwarded-For e, na falta dos dois, r.RemoteAddr como chave. Com limite de 3 requisições por minuto, dois testes deixaram passar 10 de 10. Descreva os dois furos e como a versão atual os fecha.

Ver resposta

✓ Resposta: O primeiro furo é confiar em cabeçalhos que o próprio cliente escreve. Sem um proxy à frente que os sobrescreva, X-Forwarded-For e X-Real-IP contêm o que o cliente quiser, e um atacante envia um valor diferente a cada requisição; cada valor vira uma chave nova, com cota nova. No teste, dez requisições pela mesma conexão, cada uma com um X-Forwarded-For inventado, passaram todas. O segundo furo é mais sutil: r.RemoteAddr tem o formato IP:porta, e a porta de origem muda a cada conexão nova. Dez requisições do mesmo IP com portas diferentes também passaram todas, porque cada combinação era uma chave diferente. A versão atual usa net.SplitHostPort(r.RemoteAddr) e fica só com o IP, ignorando os cabeçalhos; no mesmo teste, o mesmo IP com portas variadas foi limitado a 3 de 10. Quando o servidor fica atrás de um proxy conhecido, aí sim o IP real vem de X-Forwarded-For, mas do valor que o proxy acrescentou, o último da lista, e nunca do primeiro, que é o que o cliente mandou.

Exercício 3

O middleware de rate limiting respondia 429 com o cabeçalho Retry-After: 1m0s. Clientes bem-comportados, que respeitam esse cabeçalho, passaram a repetir as chamadas imediatamente, agravando a sobrecarga. Por quê?

Ver resposta

✓ Resposta: Porque 1m0s não é um valor válido para Retry-After. O padrão HTTP define dois formatos: um número inteiro de segundos, como 60, ou uma data HTTP completa, como Wed, 30 Sep 2026 20:00:00 GMT. O texto 1m0s vinha de janela.String(), que formata um time.Duration do jeito do Go, e é esse o valor medido na versão original. Um cliente que tenta interpretar o cabeçalho e falha costuma cair no comportamento padrão da biblioteca, que muitas vezes é tentar de novo logo, e justamente os clientes mais cuidadosos passam a martelar o servidor. A versão atual envia fmt.Sprintf("%d", int(janela.Seconds())), o que produz 60. O mesmo cuidado vale para os cabeçalhos X-RateLimit-*: não são padronizados, e a janela como texto do Go dificulta a leitura por outras linguagens. Há uma proposta de padrão com os cabeçalhos RateLimit e RateLimit-Policy, que usam segundos. A regra geral é que cabeçalhos são contrato com clientes de qualquer linguagem, e não saída de String().

Exercício 4

O responderErroAuth monta a resposta com fmt.Fprintf(w, `{"erro":"%s"}`, msg), e o middleware JWT passa como mensagem "token inválido: " + err.Error(). Aponte dois problemas dessa combinação, um de formato e um de segurança.

Ver resposta

✓ Resposta: O problema de formato é que o texto entra no JSON sem escape. Se a mensagem tiver aspas, barra invertida ou quebra de linha, o que é comum em erros que citam o valor recebido, o corpo deixa de ser JSON válido, e um cliente que o decodifica recebe um erro de sintaxe no lugar da mensagem. Em casos piores, o conteúdo controlado pelo cliente pode inserir campos no objeto. A correção é deixar o encoding/json montar o corpo: json.NewEncoder(w).Encode(map[string]string{"erro": msg}), que escapa o texto corretamente. O problema de segurança é devolver err.Error() ao cliente: mensagens internas revelam como a validação funciona, qual etapa falhou, se a assinatura ou o formato, e às vezes trechos de dados. Para um atacante, isso é um guia de ajuste. A resposta ao cliente deve ser genérica, como "token inválido", e o detalhe vai para o log do servidor, junto com o request ID, para que o suporte consiga ligar uma reclamação ao registro sem expor nada.

Exercício 5

Na pilha global do exemplo, a ordem é CORS, rate limit, logging e recuperação, do mais externo para o mais interno. Um operador reclama que os ataques de força bruta barrados pelo limitador não aparecem nos logs. Explique por quê, e proponha uma ordem que registre tudo sem perder as outras garantias.

Ver resposta

✓ Resposta: O encadear aplica o primeiro middleware da lista como o mais externo, e cada requisição atravessa a cadeia de fora para dentro. Uma requisição barrada pelo rate limit recebe o 429 ali mesmo e nunca chega ao middleware de logging, que está dentro dele, e por isso os bloqueios não aparecem no log, justamente os eventos que o operador mais queria ver. A ordem mais útil costuma ser recuperação, logging, CORS, rate limit e, por último, os middlewares de rota como JWT. A recuperação por fora pega pânicos de qualquer camada, inclusive dos outros middlewares. O logging logo em seguida registra toda requisição que entra, com o status final, seja 429, 401 ou 500. O CORS antes do limitador responde aos preflights OPTIONS sem gastar a cota do cliente. Com essa ordem, a recuperação precisa ficar fora do logging, e o logging precisa registrar o status que a recuperação escreveu. Um teste com httptest que envie requisições acima do limite e confira a linha de log é a forma de garantir que a ordem não se perca numa refatoração.

Comentários

Mais em Golang

Introdução ao database/sql e drivers em Go
Introdução ao database/sql e drivers em Go

O pacote database/sql em Go: drivers e import em branco, o pool de conexões e…

Profiling e otimização de performance em Go
Profiling e otimização de performance em Go

Profiling em Go medido: benchmarks com -benchmem, pprof por HTTP e em testes…

Métodos em Structs: receivers por valor e por ponteiro
Métodos em Structs: receivers por valor e por ponteiro

Métodos em Go: receiver por valor e por ponteiro, a conversão automática para…