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
-
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/context
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.