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