Go

Middleware de Autenticação, Logging e Rate Limiting em Go na Prática

15 min de leitura

Middleware de Autenticação, Logging e Rate Limiting em Go na Prática

Introdução: Por que Middleware Importa em Go 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. 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 por que funcionam dessa forma e como combiná-los em uma pipeline real. Middleware de Autenticação com JWT Entendendo o Fluxo de Autenticação 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

<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: &quot;Quem é você?&quot;. 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 (

&quot;context&quot;

&quot;errors&quot;

&quot;fmt&quot;

&quot;net/http&quot;

&quot;strings&quot;

&quot;time&quot;

&quot;github.com/golang-jwt/jwt/v5&quot;

)

// Claims define a estrutura das informações no JWT

type Claims struct {

UserID string json:&quot;user_id&quot;

Username string json:&quot;username&quot;

Role string json:&quot;role&quot;

jwt.RegisteredClaims

}

var jwtSecret = []byte(&quot;sua-chave-secreta-super-segura-aqui&quot;)

// GenerateToken cria um JWT válido por 24 horas

func GenerateToken(userID, username, role string) (string, error) {

claims := &amp;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 := &amp;Claims{}

token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {

if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

return nil, errors.New(&quot;metodo de assinatura inesperado&quot;)

}

return jwtSecret, nil

})

if err != nil || !token.Valid {

return nil, errors.New(&quot;token inválido ou expirado&quot;)

}

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(&quot;Authorization&quot;)

if authHeader == &quot;&quot; {

http.Error(w, &quot;Token não fornecido&quot;, http.StatusUnauthorized)

return

}

// Esperamos formato: &quot;Bearer &lt;token&gt;&quot;

parts := strings.Split(authHeader, &quot; &quot;)

if len(parts) != 2 || parts[0] != &quot;Bearer&quot; {

http.Error(w, &quot;Formato de token inválido&quot;, http.StatusUnauthorized)

return

}

claims, err := ValidateToken(parts[1])

if err != nil {

http.Error(w, fmt.Sprintf(&quot;Falha na validação: %v&quot;, err), http.StatusUnauthorized)

return

}

// Armazena claims no contexto para acesso posterior

ctx := context.WithValue(r.Context(), &quot;claims&quot;, claims)

next.ServeHTTP(w, r.WithContext(ctx))

})

}

// Exemplo de handler protegido

func protectedHandler(w http.ResponseWriter, r *http.Request) {

claims := r.Context().Value(&quot;claims&quot;).(*Claims)

w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)

fmt.Fprintf(w, {&quot;message&quot;:&quot;Bem-vindo %s&quot;,&quot;role&quot;:&quot;%s&quot;}, claims.Username, claims.Role)

}

func loginHandler(w http.ResponseWriter, r *http.Request) {

token, _ := GenerateToken(&quot;user123&quot;, &quot;joao&quot;, &quot;admin&quot;)

w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)

fmt.Fprintf(w, {&quot;token&quot;:&quot;%s&quot;}, 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 (

&quot;log/slog&quot;

&quot;net/http&quot;

&quot;time&quot;

)

// 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 := &amp;responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

// Executa o handler

next.ServeHTTP(rw, r)

// Registra informações

duration := time.Since(start).Milliseconds()

slog.Info(&quot;requisição processada&quot;,

slog.String(&quot;método&quot;, r.Method),

slog.String(&quot;caminho&quot;, r.RequestURI),

slog.Int(&quot;status&quot;, rw.statusCode),

slog.Int64(&quot;duração_ms&quot;, duration),

slog.String(&quot;ip_cliente&quot;, r.RemoteAddr),

slog.String(&quot;user_agent&quot;, r.Header.Get(&quot;User-Agent&quot;)),

)

})

}</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 &quot;bursts&quot; 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 &quot;balde&quot; 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 (

&quot;fmt&quot;

&quot;net/http&quot;

&quot;sync&quot;

&quot;time&quot;

)

// 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 := &amp;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 := &amp;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 &gt;= 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) &gt; t.cleanupTime {

delete(t.buckets, clientID)

}

bucket.mu.Unlock()

}

}

func min(a, b float64) float64 {

if a &lt; 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(&quot;claims&quot;).(*Claims); ok {

clientID = claims.UserID

}

if !limiter.Allow(clientID) {

w.Header().Set(&quot;Retry-After&quot;, &quot;60&quot;)

http.Error(w, &quot;Muitas requisições, tente novamente mais tarde&quot;, 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 (

&quot;fmt&quot;

&quot;net/http&quot;

)

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(&quot;/health&quot;, func(w http.ResponseWriter, r *http.Request) {

fmt.Fprintf(w, &quot;OK&quot;)

})

// Rota de login (logging + rate limit)

loginRoute := LoggingMiddleware(

RateLimitMiddleware(limiter)(

http.HandlerFunc(loginHandler),

),

)

mux.Handle(&quot;/login&quot;, loginRoute)

// Rota protegida (logging + rate limit + autenticação)

protectedRoute := LoggingMiddleware(

RateLimitMiddleware(limiter)(

AuthMiddleware(

http.HandlerFunc(protectedHandler),

),

),

)

mux.Handle(&quot;/protected&quot;, protectedRoute)

// Alternativamente, função helper para evitar aninhamento

protectedRoute2 := Chain(

http.HandlerFunc(protectedHandler),

AuthMiddleware,

RateLimitMiddleware(limiter),

LoggingMiddleware,

)

mux.Handle(&quot;/api/profile&quot;, protectedRoute2)

// Inicia servidor

fmt.Println(&quot;Servidor rodando em :8080&quot;)

http.ListenAndServe(&quot;:8080&quot;, 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 &gt;= 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 (

&quot;fmt&quot;

&quot;net/http&quot;

&quot;net/http/httptest&quot;

&quot;testing&quot;

)

func TestAuthMiddlewareSuccess(t *testing.T) {

token, _ := GenerateToken(&quot;user1&quot;, &quot;test&quot;, &quot;admin&quot;)

handler := AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

claims := r.Context().Value(&quot;claims&quot;).(*Claims)

if claims.UserID != &quot;user1&quot; {

t.Errorf(&quot;UserID esperado: user1, obtido: %s&quot;, claims.UserID)

}

w.WriteHeader(http.StatusOK)

fmt.Fprintf(w, &quot;OK&quot;)

}))

req := httptest.NewRequest(&quot;GET&quot;, &quot;/protected&quot;, nil)

req.Header.Set(&quot;Authorization&quot;, fmt.Sprintf(&quot;Bearer %s&quot;, token))

rw := httptest.NewRecorder()

handler.ServeHTTP(rw, req)

if rw.Code != http.StatusOK {

t.Errorf(&quot;Status esperado: 200, obtido: %d&quot;, 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(&quot;GET&quot;, &quot;/protected&quot;, nil)

rw := httptest.NewRecorder()

handler.ServeHTTP(rw, req)

if rw.Code != http.StatusUnauthorized {

t.Errorf(&quot;Status esperado: 401, obtido: %d&quot;, 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 := &quot;test-client&quot;

// Primeiras 2 requisições devem passar

for i := 0; i &lt; 2; i++ {

req := httptest.NewRequest(&quot;GET&quot;, &quot;/&quot;, nil)

req.RemoteAddr = clientID

rw := httptest.NewRecorder()

handler.ServeHTTP(rw, req)

if rw.Code != http.StatusOK {

t.Errorf(&quot;Requisição %d: esperado 200, obtido %d&quot;, i+1, rw.Code)

}

}

// Terceira deve ser bloqueada

req := httptest.NewRequest(&quot;GET&quot;, &quot;/&quot;, nil)

req.RemoteAddr = clientID

rw := httptest.NewRecorder()

handler.ServeHTTP(rw, req)

if rw.Code != http.StatusTooManyRequests {

t.Errorf(&quot;Requisição bloqueada: esperado 429, obtido %d&quot;, 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Stack vs Heap em Go: Escape Analysis e Alocação Eficiente: Do Básico ao Avançado
Stack vs Heap em Go: Escape Analysis e Alocação Eficiente: Do Básico ao Avançado

Fundamentos de Stack e Heap em Go A memória em qualquer programa está organiz...

Como Usar database/sql em Go: Conexão, Queries e Boas Práticas Nativas em Produção
Como Usar database/sql em Go: Conexão, Queries e Boas Práticas Nativas em Produção

Fundamentos do Package database/sql O package é a abstração padrão da linguag...

O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução
O que Todo Dev Deve Saber sobre Type Switch em Go: Discriminando Tipos em Tempo de Execução

O que é Type Switch e Por que Usar Type switch é um mecanismo em Go que permi...