Go: Projeto Integrado: API de comércio eletrônico (Parte 2)
Última atualização: 2026-08-26
Na parte anterior, configuramos a estrutura para a API de comércio eletrônico. O objetivo desta parte é torná-la segura, fácil de manter e testável.
A API de comércio eletrônico do Bob revelou sérios problemas durante uma auditoria de segurança: ausência de autenticação, de controles de acesso e de limitação de solicitações. É hora de reforçar o pipeline de middleware.
1. Você aprenderá
- Integração do middleware de autenticação JWT
- RBAC (Controle de Acesso Baseado em Funções)
- Pipeline global de middleware (Registro + Recuperação + Autenticação + RBAC + Limite de Taxa)
- Migração de banco de dados SQL (golang-migrate)
- Testes de integração (httptest + banco de dados de teste)
- Middleware de paginação
2. Relato: Relatório de auditoria de segurança
(1) Problema: “Nenhuma das APIs exige autenticação, portanto, qualquer pessoa pode excluir produtos.”
No primeiro dia em que o MVP do Bob entrou em operação, a equipe de segurança enviou um relatório de auditoria:
“Vulnerabilidade grave: a chamada DELETE /api/v1/products/1 não exige autenticação. Qualquer pessoa pode excluir produtos. Vulnerabilidade moderada: não há limitação da taxa de solicitações; um invasor poderia realizar um ataque de força bruta na API de login. Recomendação: autenticação JWT + permissões RBAC + limitação da taxa de solicitações.”
Bob deu uma olhada na porta dos fundos deixada pela versão anterior:
// Bad code: no authentication, anyone can place an order
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
userID := 1 // Hardcoded! All orders go to user 1
// Business logic...
}
(2) Objetivo da aula: Concluir o pipeline de middleware
Request → Recovery → Logging → CORS → Auth → RBAC → RateLimit → Handler
Planos para esta semana:
- Middleware de autenticação JWT (substitui o userID codificado)
- Controle de permissões RBAC (funções de administrador/cliente)
- Migração de banco de dados SQL (gerenciamento de esquema de tabelas com controle de versão)
- Testes de integração (httptest + banco de dados temporário)
- Middleware de paginação
3. Implementação completa
(1) ▶ Exemplo: JWT Toolkit
// internal/auth/jwt.go
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
type Claims struct {
UserID int `json:"user_id"`
Role string `json:"role"`
Email string `json:"email"`
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type jwtService struct {
secret []byte
accessTTL time.Duration
refreshTTL time.Duration
}
func NewJWTService(secret string) *jwtService {
return &jwtService{
secret: []byte(secret),
accessTTL: 15 * time.Minute,
refreshTTL: 7 * 24 * time.Hour,
}
}
func (s *jwtService) GenerateToken(claims Claims) (string, error) {
header := map[string]string{"alg": "HS256", "typ": "JWT"}
headerJSON, _ := json.Marshal(header)
payload := map[string]interface{}{
"user_id": claims.UserID,
"role": claims.Role,
"email": claims.Email,
"exp": time.Now().Add(s.accessTTL).Unix(),
"iat": time.Now().Unix(),
}
payloadJSON, _ := json.Marshal(payload)
headerEnc := base64URLEncode(headerJSON)
payloadEnc := base64URLEncode(payloadJSON)
message := headerEnc + "." + payloadEnc
mac := hmac.New(sha256.New, s.secret)
mac.Write([]byte(message))
sig := base64URLEncode(mac.Sum(nil))
return message + "." + sig, nil
}
func (s *jwtService) ValidateToken(token string) (*Claims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token")
}
message := parts[0] + "." + parts[1]
mac := hmac.New(sha256.New, s.secret)
mac.Write([]byte(message))
expected := base64URLEncode(mac.Sum(nil))
if !hmac.Equal([]byte(parts[2]), []byte(expected)) {
return nil, fmt.Errorf("invalid signature")
}
payloadJSON, err := base64URLDecode(parts[1])
if err != nil {
return nil, err
}
var payload struct {
UserID int `json:"user_id"`
Role string `json:"role"`
Email string `json:"email"`
Exp float64 `json:"exp"`
}
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return nil, err
}
if time.Now().Unix() > int64(payload.Exp) {
return nil, fmt.Errorf("token expired")
}
return &Claims{
UserID: payload.UserID,
Role: payload.Role,
Email: payload.Email,
}, nil
}
func base64URLEncode(data []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
}
func base64URLDecode(s string) ([]byte, error) {
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}
(2) ▶ Exemplo: Pipeline de middleware
// internal/middleware/middleware.go
package middleware
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"ecommerce/internal/auth"
)
type contextKey string
const UserClaimsKey contextKey = "user_claims"
// ---------- Middleware type ----------
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// ---------- Recovery ----------
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
writeError(w, http.StatusInternalServerError, "internal error")
}
}()
next.ServeHTTP(w, r)
})
}
// ---------- Logging ----------
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// ---------- CORS ----------
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// ---------- Auth (JWT) ----------
func Auth(jwtService *auth.JWTService) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
writeError(w, http.StatusUnauthorized, "missing token")
return
}
token := authHeader[7:]
claims, err := jwtService.ValidateToken(token)
if err != nil {
writeError(w, http.StatusUnauthorized, err.Error())
return
}
ctx := context.WithValue(r.Context(), UserClaimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---------- RBAC ----------
func RequireRole(roles ...string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(UserClaimsKey).(*auth.Claims)
if !ok {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
for _, role := range roles {
if claims.Role == role {
next.ServeHTTP(w, r)
return
}
}
writeError(w, http.StatusForbidden, "insufficient permissions")
})
}
}
// ---------- Token bucket rate limiting ----------
type TokenBucket struct {
mu sync.Mutex
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
}
type IPRateLimiter struct {
mu sync.RWMutex
buckets map[string]*TokenBucket
rate float64
burst int
}
var globalLimiter = NewIPRateLimiter(100, 200)
func NewIPRateLimiter(rate float64, burst int) *IPRateLimiter {
return &IPRateLimiter{
buckets: make(map[string]*TokenBucket),
rate: rate,
burst: burst,
}
}
func (rl *IPRateLimiter) getBucket(ip string) *TokenBucket {
rl.mu.Lock()
defer rl.mu.Unlock()
b, ok := rl.buckets[ip]
if !ok {
b = &TokenBucket{
tokens: float64(rl.burst),
maxTokens: float64(rl.burst),
refillRate: rl.rate,
lastRefill: time.Now(),
}
rl.buckets[ip] = b
}
return b
}
func (b *TokenBucket) allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
elapsed := now.Sub(b.lastRefill).Seconds()
b.tokens = min(b.tokens+elapsed*b.refillRate, b.maxTokens)
b.lastRefill = now
if b.tokens >= 1 {
b.tokens--
return true
}
return false
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if !globalLimiter.getBucket(ip).allow() {
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
// ---------- Pagination ----------
type Pagination struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Offset int `json:"-"`
}
func ParsePagination(r *http.Request) Pagination {
page := 1
perPage := 20
if p := r.URL.Query().Get("page"); p != "" {
if v, err := parseInt(p); err == nil && v > 0 {
page = v
}
}
if pp := r.URL.Query().Get("per_page"); pp != "" {
if v, err := parseInt(pp); err == nil && v > 0 && v <= 100 {
perPage = v
}
}
return Pagination{
Page: page,
PerPage: perPage,
Offset: (page - 1) * perPage,
}
}
func parseInt(s string) (int, error) {
var n int
for _, c := range s {
if c < '0' || c > '9' {
return 0, fmt.Errorf("not a number")
}
n = n*10 + int(c-'0')
}
return n, nil
}
// ---------- Utility ----------
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
(3) ▶ Exemplo: Migração de banco de dados SQL
// internal/migration/migration.go
package migration
import (
"database/sql"
"fmt"
"log"
)
type Migration struct {
Version int
SQL string
}
var migrations = []Migration{
{
Version: 1,
SQL: `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'customer',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
},
{
Version: 2,
SQL: `CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
category_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
)`,
},
{
Version: 3,
SQL: `CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
total_price REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)`,
},
{
Version: 4,
SQL: `CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
},
}
func RunMigrations(db *sql.DB) error {
// Create version table
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return fmt.Errorf("create migrations table: %w", err)
}
for _, m := range migrations {
// Check if already applied
var exists bool
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = ?)", m.Version).Scan(&exists)
if err != nil {
return err
}
if exists {
continue
}
// Apply migration
if _, err := db.Exec(m.SQL); err != nil {
return fmt.Errorf("migration %d: %w", m.Version, err)
}
// Record version
if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", m.Version); err != nil {
return err
}
log.Printf("[Migration] Version %d applied", m.Version)
}
return nil
}
(4) ▶ Exemplo: Testes de integração
⚙️ Pré-requisito: Execute
go get github.com/mattn/go-sqlite3(requer CGO; como alternativa, usemodernc.org/sqlitepara um driver exclusivamente de Go)
// internal/handler/integration_test.go
package handler
import (
"bytes"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"ecommerce/internal/auth"
"ecommerce/internal/middleware"
"ecommerce/internal/repository"
"ecommerce/internal/service"
_ "github.com/mattn/go-sqlite3"
)
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
// Initialize tables
_, err = db.Exec(`
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT 'customer');
CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, price REAL NOT NULL, stock INTEGER DEFAULT 0);
CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL, total_price REAL NOT NULL, status TEXT DEFAULT 'pending');
`)
if err != nil {
t.Fatal(err)
}
return db
}
func setupTestApp(db *sql.DB) http.Handler {
userRepo := repository.NewUserRepository(db)
productRepo := repository.NewProductRepository(db)
orderRepo := repository.NewOrderRepository(db)
userSvc := service.NewUserService(userRepo)
productSvc := service.NewProductService(productRepo)
orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)
userHandler := NewUserHandler(userSvc)
productHandler := NewProductHandler(productSvc)
orderHandler := NewOrderHandler(orderSvc)
mux := http.NewServeMux()
userHandler.Register(mux)
productHandler.Register(mux)
orderHandler.Register(mux)
// Apply middleware
return middleware.Chain(mux,
middleware.Recovery,
middleware.Logging,
middleware.CORS,
)
}
func TestRegisterAndLogin(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
app := setupTestApp(db)
// 1. Register
registerBody := `{"email":"test@example.com","password":"password123","name":"Test User"}`
req := httptest.NewRequest("POST", "/api/v1/register", bytes.NewBufferString(registerBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
app.ServeHTTP(resp, req)
if resp.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.Code)
}
// 2. Login
loginBody := `{"email":"test@example.com","password":"password123"}`
req = httptest.NewRequest("POST", "/api/v1/login", bytes.NewBufferString(loginBody))
req.Header.Set("Content-Type", "application/json")
resp = httptest.NewRecorder()
app.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.Code)
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
data := result["data"].(map[string]interface{})
if data["email"] != "test@example.com" {
t.Errorf("expected test@example.com, got %v", data["email"])
}
}
func TestCreateOrder_RequiresAuth(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
app := setupTestApp(db)
// Request without Token should return 401
orderBody := `{"product_id":1,"quantity":1}`
req := httptest.NewRequest("POST", "/api/v1/orders", bytes.NewBufferString(orderBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
// Note: The current version does not have Auth middleware;
// it needs to be added in the integration test environment
_ = resp
_ = app
}
sequenceDiagram
participant Client
participant MW as Middleware Pipeline
participant AuthS as Auth Service
participant H as Handler
Client->>MW: POST /api/v1/orders (Bearer token)
MW->>MW: Recovery / Logging / CORS
MW->>AuthS: Auth middleware validates JWT
AuthS-->>MW: Claims {user_id, role}
MW->>MW: RBAC checks customer role
MW->>MW: RateLimit check
MW->>H: Passed! Inject user_id into Context
H->>H: Create order (using user_id)
H-->>MW: 201 Created
MW-->>Client: JSON response
(5) A solicitação passa pelo pipeline de middleware
Request
↓ Recovery (catch panic)
↓ Logging (log + duration)
↓ CORS (cross-origin)
↓ Auth (JWT verification → inject Claims)
↓ RBAC (check role)
↓ RateLimit (token bucket)
↓ Handler (business logic)
Response
❓ Perguntas Frequentes
P: Como o JWT é integrado ao middleware? R: O middleware Auth analisa o token Bearer no cabeçalho, chama
jwtService.ValidateTokene injeta oClaimsno contexto da solicitação usandocontext.WithValue. Os manipuladores subsequentes recuperam as informações do usuário usandor.Context().Value(middleware.UserClaimsKey).
P: Como o RBAC é implementado? R: Após o middleware Auth, o middleware RequireRole verifica se o campo
rolenas Claims está na lista de funções permitidas. Implementações mais complexas podem incorporar uma matriz de permissões (na qual cada função possui uma lista de permissões, e o middleware verifica permissões específicas).
P: Como escolho uma ferramenta de migração SQL? R: (1)
golang-migrate/migrate— a mais utilizada, compatível com vários bancos de dados e fontes de migração; (2) Migração manual — a abordagem utilizada nesta lição, adequada para projetos pequenos; (3)pressly/goose— com diversos recursos. Para projetos grandes, recomendamosgolang-migrate.
P: Como faço para escrever um teste de integração? R: Use
httptest.NewServerouhttptest.NewRecorderpara simular solicitações HTTP, em combinação com um banco de dados temporário (SQLite no modo:memory:). Fluxo de teste: configurar o banco de dados → injetar dependências → criar um manipulador → enviar uma solicitação HTTP → verificar a resposta.
P: Como o middleware de paginação é implementado? R: Ele analisa os parâmetros de consulta
pageeper_pagepara calcular o deslocamento. Em seguida, adicionaLIMIT ? OFFSET ?à consulta na camada de Serviço. O Handler pode adicionar um cabeçalhoX-Total-Countou metainformações à resposta. O middleware é responsável apenas por analisar os parâmetros; ele não modifica a lógica da consulta.
📖 Resumo
- Middleware de autenticação JWT: analisar tokens, inserir reivindicações
- RBAC: O middleware RequireRole verifica as funções
- Pipeline de middleware: Recuperação → Registro em log → CORS → Autenticação → RBAC → Limite de taxa
- Migrações SQL: gerenciamento com controle de versão das alterações na estrutura das tabelas
- Testes de integração: httptest + banco de dados temporário
- Paginação: Análise do parâmetro
query+LIMIT/OFFSET
📝 Exercícios
-
Básico (Dificuldade ⭐): Integre o middleware de autenticação desta lição à API de comércio eletrônico da lição anterior. Retorne um token JWT no momento do cadastro e exija um token Bearer para todos os endpoints
/api/v1/orders/*. Retorne um erro 401 para solicitações sem token. -
Avançado (Dificuldade ⭐⭐): Implemente um teste de integração completo para o pipeline de middleware. Requisitos: (1) Use
httptest+ um banco de dados SQLite temporário; (2) Teste o fluxo de autenticação (cadastro → login → obtenção do token → acesso a endpoints protegidos usando o token); (3) Teste cenários de negação de permissão (um “cliente” tentando acessar um endpoint “admin”); (4) Teste cenários de limitação de taxa (um grande número de solicitações em um curto período retornando um erro 429). -
Desafio (Dificuldade ⭐⭐⭐): Implementar a migração do banco de dados usando o golang-migrate. Requisitos: (1) Instalar a CLI
golang-migrate/migrate; (2) Escrever três arquivos de migração (para criar as tabelasusers,productseorders); (3) Migrar o binário Go (pacoteembed); (4) Verifique se ocmd/migrate/main.gosuporta os comandosupedown; (5) Realize testes de integração usando o esquema do banco de dados migrado.