<h2>Introdução: Por que Middleware Importa em Go</h2>
<p>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.</p>
<p>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 <strong>por que funcionam dessa forma</strong> e como combiná-los em uma pipeline real.</p>
<h2>Middleware de Autenticação com JWT</h2>
<h3>Entendendo o Fluxo de Autenticação</h3>
<p>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.</p>
<p>O fluxo é: cliente envia token no header <code>Authorization</code>, 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.</p>
<h3>Implementação Prática de Autenticação</h3>
<pre><code class="language-go">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)
}</code></pre>
<p><strong>O que está acontecendo aqui:</strong> O middleware intercepta toda requisição, extrai o token do header <code>Authorization</code>, 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.</p>
<h2>Middleware de Logging Estruturado</h2>
<h3>Por Que Logging Estruturado?</h3>
<p>Logging simples com <code>fmt.Println</code> 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.</p>
<p>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 <code>log/slog</code> do Go (disponível desde Go 1.21), que é a abordagem padrão moderna.</p>
<h3>Implementação com Slog</h3>
<pre><code class="language-go">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")),
)
})
}</code></pre>
<p><strong>Por que wrappear o ResponseWriter?</strong> Por padrão, você não sabe qual status HTTP foi enviado sem interceptar a chamada <code>WriteHeader()</code>. Envolvemos o writer original e capturamos esse valor, permitindo logar informações completas sobre a resposta.</p>
<h2>Middleware de Rate Limiting</h2>
<h3>Estratégias de Rate Limiting</h3>
<p>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 <strong>token bucket</strong> porque é elegante, justo e eficiente.</p>
<p>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.</p>
<h3>Implementação com Token Bucket</h3>
<pre><code class="language-go">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)
})
}
}</code></pre>
<p><strong>Detalhe importante:</strong> Ao validar autenticação antes de aplicar rate limiting, você pode usar o <code>userID</code> 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.</p>
<h2>Composição de Middlewares em Uma Pipeline Real</h2>
<h3>Ordem Importa</h3>
<p>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.</p>
<pre><code class="language-go">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
}</code></pre>
<p><strong>O padrão de composição:</strong> Cada middleware é uma função que recebe um <code>http.Handler</code> e retorna outro <code>http.Handler</code>. Essa é a beleza da programação funcional em Go — você combina pequenas funções especializadas para criar comportamentos complexos sem frameworks pesados.</p>
<h2>Testando Middlewares</h2>
<h3>Testes Unitários Práticos</h3>
<pre><code class="language-go">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)
}
}</code></pre>
<p><strong>Teste sempre seus middlewares isoladamente:</strong> Use <code>httptest.NewRequest()</code> e <code>httptest.NewRecorder()</code> para simular requisições HTTP sem depender de um servidor real. Isso torna seus testes rápidos e confiáveis.</p>
<h2>Conclusão</h2>
<p>Você aprendeu três padrões fundamentais que qualquer API profissional precisa: <strong>autenticação stateless via JWT</strong> (validar identidade de forma segura e escalável), <strong>logging estruturado</strong> (rastrear requisições com contexto completo para debugging), e <strong>rate limiting com token bucket</strong> (proteger servidores contra abuso sem ser injusto com clientes legítimos).</p>
<p>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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://golang.org/pkg/net/http/" target="_blank" rel="noopener noreferrer">Go Official Documentation - net/http</a></li>
<li><a href="https://github.com/golang-jwt/jwt" target="_blank" rel="noopener noreferrer">golang-jwt/jwt - JWT Library</a></li>
<li><a href="https://pkg.go.dev/log/slog" target="_blank" rel="noopener noreferrer">Go 1.21 log/slog Documentation</a></li>
<li><a href="https://martinfowler.com/articles/patterns-of-distributed-systems/rate-limiting.html" target="_blank" rel="noopener noreferrer">Rate Limiting Strategies - Martin Fowler</a></li>
<li><a href="https://www.gopl.io/" target="_blank" rel="noopener noreferrer">The Go Programming Language - Chapter 8 (Concurrency)</a></li>
</ul>
<p><!-- FIM --></p>