Capítulo 10: Autenticação Stateless com JWT e Middleware no Gin
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP (Header: Authorization: Bearer <token>)"] --> B["Gin Engine"]
B --> C["Middleware AutenticacaoJWT()"]
subgraph PROCESSAMENTO_TOKEN["Validação Criptográfica do Token (golang-jwt/v5)"]
C --> D{"Formato 'Bearer <token>' Válido?"}
D -->|"Não"| E["c.AbortWithStatusJSON(401 Unauthorized)"]
D -->|"Sim"| F["jwt.ParseWithClaims(token, &ClaimsCustomizadas{}, keyFunc)"]
F --> G{"Assinatura Válida e Não Expirado?"}
G -->|"Não"| H["c.AbortWithStatusJSON(401 Token Inválido/Expirado)"]
G -->|"Sim"| I["c.Set('userID', claims.UserID) + c.Set('role', claims.Role)"]
end
I --> J["c.Next() -> Encaminha para Handler Protegido"]
J --> K["Handler lê dados via c.Get('userID')"]
K --> L["HTTP 200 OK + Resposta de Negócio"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style E fill:#ffebee,stroke:#e53935,stroke-width:2px
style F fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style G fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style H fill:#ffebee,stroke:#e53935,stroke-width:2px
style I fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style J fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style K fill:#e0f2f1,stroke:#009688,stroke-width:2px
style L fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
🏛️ 1. A Arquitetura do JWT (RFC 7519) em Microsserviços
Em sistemas distribuídos de alta concorrência construídos em Go, manter sessões de autenticação na memória do servidor com cookies de sessão tradicionais impede a escalabilidade horizontal: se a requisição bater em outra réplica, a sessão não existirá, exigindo bancos de sessões compartilhados ou balanceamento com afinidade (sticky sessions).
O JSON Web Token (JWT) viabiliza o paradigma Stateless (Sem Estado): todas as informações de identidade do usuário necessárias para autorizar a requisição trafegam assinadas criptograficamente dentro do próprio token. O servidor não precisa consultar o banco de dados a cada requisição HTTP para saber quem é o usuário.
Um token JWT é composto por três partes delimitadas por ponto (.):
- Header (Cabeçalho): Informa o tipo de token (
JWT) e o algoritmo criptográfico de assinatura (ex:HS256,RS256). - Payload (Claims): Conjunto de declarações de identidade contendo claims registradas (
iss,exp,sub,iat) e claims privadas customizadas (user_id,role,tenant_id). - Signature (Assinatura): Hash criptográfico gerado a partir da concatenação do Header com o Payload e de uma chave secreta privada, impedindo qualquer modificação maliciosa do payload no trânsito.
Algoritmos Simétricos vs Assimétricos
| Algoritmo | Tipo de Chave | Onde Usar | Vantagens / Desvantagens |
|---|---|---|---|
| HS256 (HMAC-SHA256) | Chave Simétrica Única | Monólitos e APIs internas | Ultra-rápido, mas todas as instâncias devem conhecer o segredo compartilhado. |
| RS256 / ES256 (RSA / ECDSA) | Par Chave Pública / Privada | Arquitetura de Microsserviços | Apenas o Auth Service possui a Chave Privada para assinar; todos os outros microsserviços validam usando apenas a Chave Pública. |
⚙️ 2. Implementação Canônica com golang-jwt/jwt/v5
A versão 5 da biblioteca oficial github.com/golang-jwt/jwt/v5 introduziu melhorias de segurança estritas contra o ataque de algoritmo nulo (None Algorithm Exploit):
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var segredoJWT = []byte("chave-super-secreta-corporativa-2026")
// ClaimsCustomizadas estende as claims padrão da RFC 7519
type ClaimsCustomizadas struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// GerarToken emite um novo JWT assinado com validade de 2 horas
func GerarToken(userID uint, email, role string) (string, error) {
claims := ClaimsCustomizadas{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "api-corporativa-gin",
Subject: email,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(segredoJWT)
}
// ValidarToken inspeciona a assinatura e desempacota as claims
func ValidarToken(tokenString string) (*ClaimsCustomizadas, error) {
token, err := jwt.ParseWithClaims(tokenString, &ClaimsCustomizadas{}, func(t *jwt.Token) (interface{}, error) {
// Proteção estrita contra adulteração de algoritmo no header
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("método de assinatura inesperado")
}
return segredoJWT, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*ClaimsCustomizadas); ok && token.Valid {
return claims, nil
}
return nil, errors.New("token inválido ou com claims corrompidas")
}
🛡️ 3. Middleware de Autenticação no Gin
O middleware intercepta o header HTTP, valida o formato Bearer e popula o contexto (c.Set) para consumo imediato nos endpoints seguintes:
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"meu-modulo/auth"
)
func AutenticacaoJWT() gin.HandlerFunc {
return func(c *gin.Context) {
cabecalhoAuth := c.GetHeader("Authorization")
if cabecalhoAuth == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"erro": "cabeçalho de autorização não fornecido",
})
return
}
partes := strings.Split(cabecalhoAuth, " ")
if len(partes) != 2 || strings.ToLower(partes[0]) != "bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"erro": "formato de cabeçalho inválido. Utilize 'Bearer <token>'",
})
return
}
claims, err := auth.ValidarToken(partes[1])
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"erro": "token de acesso inválido ou expirado",
"detalhes": err.Error(),
})
return
}
// Injeta as informações do usuário autenticado no contexto da requisição
c.Set("userID", claims.UserID)
c.Set("userEmail", claims.Email)
c.Set("userRole", claims.Role)
c.Next()
}
}
🔒 4. Padrão Dual-Token: Access Token e Refresh Token
Em conformidade com as diretrizes do OWASP, tokens JWT não devem ter tempo de expiração prolongado (ex: dias ou semanas), pois se forem interceptados não há mecanismo nativo de revogação imediata.
A arquitetura canônica adota o Padrão Dual-Token:
- Access Token: Tempo de vida curto (15 a 30 minutos). Utilizado diretamente nas chamadas da API.
- Refresh Token: Tempo de vida longo (7 a 30 dias). Armazenado com segurança (HttpOnly Cookie) e persistido no Redis. Quando o Access Token expira, o cliente solicita um novo par de tokens sem exigir nova digitação de senha pelo usuário.