Урок 12: Middleware, CORS, rate limiting, валидация

Урок 12. Middleware, CORS, rate limiting, валидация

🔄 Node.js → Go (ключевые аналоги):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-middleware && cd go-middleware
go mod init go-middleware

# Устанавливаем validator
go get github.com/go-playground/validator/v10
go get github.com/google/uuid
go mod tidy

💻 Код программы

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "runtime/debug"
    "strings"
    "sync"
    "syscall"
    "time"

    "github.com/go-playground/validator/v10"
    "github.com/google/uuid"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  1. ТИПЫ ДЛЯ КОНТЕКСТА                                 ║
// ╚══════════════════════════════════════════════════════════╝

type contextKey string

const (
    requestIDKey contextKey = "requestID"
    loggerKey    contextKey = "logger"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  2. СТРУКТУРЫ ОТВЕТОВ                                  ║
// ╚══════════════════════════════════════════════════════════╝

type ErrorResponse struct {
    Error   string `json:"error"`
    Message string `json:"message"`
}

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

type ValidationErrorResponse struct {
    Error  string            `json:"error"`
    Fields []ValidationError `json:"fields"`
}

// ╔══════════════════════════════════════════════════════════╗
// ║  3. RATE LIMITER (token bucket)                        ║
// ╚══════════════════════════════════════════════════════════╝

// RateLimiter — простой token bucket rate limiter на IP
type RateLimiter struct {
    mu      sync.Mutex
    buckets map[string]*bucket
    rate    int           // токенов в секунду
    burst   int           // максимальный всплеск
}

type bucket struct {
    tokens   float64
    lastTime time.Time
}

// NewRateLimiter — rate токенов/сек, burst — размер ведра
func NewRateLimiter(rate, burst int) *RateLimiter {
    rl := &RateLimiter{
        buckets: make(map[string]*bucket),
        rate:    rate,
        burst:   burst,
    }
    // Очистка старых бакетов каждую минуту
    go rl.cleanup()
    return rl
}

// Allow — проверяет, можно ли выполнить запрос
func (rl *RateLimiter) Allow(key string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    b, ok := rl.buckets[key]
    if !ok {
        b = &bucket{
            tokens:   float64(rl.burst),
            lastTime: time.Now(),
        }
        rl.buckets[key] = b
    }

    // Добавляем токены за прошедшее время
    elapsed := time.Since(b.lastTime).Seconds()
    b.tokens += elapsed * float64(rl.rate)
    if b.tokens > float64(rl.burst) {
        b.tokens = float64(rl.burst)
    }
    b.lastTime = time.Now()

    // Проверяем, есть ли токен
    if b.tokens >= 1 {
        b.tokens--
        return true
    }
    return false
}

// cleanup — удаляет старые бакеты
func (rl *RateLimiter) cleanup() {
    for {
        time.Sleep(time.Minute)
        rl.mu.Lock()
        for key, b := range rl.buckets {
            if time.Since(b.lastTime) > 5*time.Minute {
                delete(rl.buckets, key)
            }
        }
        rl.mu.Unlock()
    }
}

// ╔══════════════════════════════════════════════════════════╗
// ║  4. MIDDLEWARE                                          ║
// ╚══════════════════════════════════════════════════════════╝

// Middleware — функция, принимающая http.Handler и возвращающая http.Handler
type Middleware func(http.Handler) http.Handler

// Chain — объединяет несколько middleware в цепочку
func Chain(middlewares ...Middleware) Middleware {
    return func(next http.Handler) http.Handler {
        for i := len(middlewares) - 1; i >= 0; i-- {
            next = middlewares[i](next)
        }
        return next
    }
}

// ==========================================
// 4.1 Request ID Middleware
// ==========================================

func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Проверяем, пришёл ли ID от клиента
        reqID := r.Header.Get("X-Request-ID")
        if reqID == "" {
            reqID = uuid.New().String()
        }

        // Кладём в контекст
        ctx := context.WithValue(r.Context(), requestIDKey, reqID)

        // Возвращаем клиенту
        w.Header().Set("X-Request-ID", reqID)

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

// ==========================================
// 4.2 Logging Middleware
// ==========================================

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        reqID := GetRequestID(r.Context())

        // Логируем запрос
        log.Printf("[%s] → %s %s %s", reqID, r.Method, r.URL.Path, r.RemoteAddr)

        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)

        // Логируем ответ
        log.Printf("[%s] ← %d %s %s (%v)",
            reqID, wrapped.statusCode, r.Method, r.URL.Path, time.Since(start))
    })
}

// ==========================================
// 4.3 Recovery Middleware
// ==========================================

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // Логируем панику и стек-трейс
                reqID := GetRequestID(r.Context())
                log.Printf("[%s] PANIC: %v\n%s", reqID, rec, debug.Stack())

                // Возвращаем 500 клиенту
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusInternalServerError)
                json.NewEncoder(w).Encode(ErrorResponse{
                    Error:   "internal_server_error",
                    Message: "An unexpected error occurred",
                })
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// ==========================================
// 4.4 CORS Middleware
// ==========================================

// CORSMiddleware — настраиваемый CORS
func CORSMiddleware(allowedOrigins []string, allowedMethods []string, allowedHeaders []string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")

            // Проверяем, разрешён ли origin
            allowed := false
            for _, o := range allowedOrigins {
                if o == "*" || o == origin {
                    allowed = true
                    break
                }
            }

            if allowed {
                w.Header().Set("Access-Control-Allow-Origin", origin)
                w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ", "))
                w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ", "))
                w.Header().Set("Access-Control-Allow-Credentials", "true")
                w.Header().Set("Access-Control-Max-Age", "86400") // 24 часа
            }

            // Preflight (OPTIONS) запрос
            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// ==========================================
// 4.5 Timeout Middleware
// ==========================================

func TimeoutMiddleware(timeout time.Duration) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), timeout)
            defer cancel()

            done := make(chan struct{})
            tw := &timeoutWriter{ResponseWriter: w}

            go func() {
                next.ServeHTTP(tw, r.WithContext(ctx))
                close(done)
            }()

            select {
            case <-done:
                // Завершилось нормально
            case <-ctx.Done():
                tw.timeout = true
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusGatewayTimeout)
                json.NewEncoder(w).Encode(ErrorResponse{
                    Error:   "timeout",
                    Message: "Request timed out",
                })
            }
        })
    }
}

// ==========================================
// 4.6 Rate Limit Middleware
// ==========================================

func RateLimitMiddleware(limiter *RateLimiter) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Используем IP как ключ
            ip, _, _ := net.SplitHostPort(r.RemoteAddr)
            if !limiter.Allow(ip) {
                w.Header().Set("Content-Type", "application/json")
                w.Header().Set("Retry-After", "1")
                w.WriteHeader(http.StatusTooManyRequests)
                json.NewEncoder(w).Encode(ErrorResponse{
                    Error:   "rate_limit_exceeded",
                    Message: "Too many requests. Please try again later.",
                })
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// ==========================================
// 4.7 Security Headers Middleware
// ==========================================

func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

// ╔══════════════════════════════════════════════════════════╗
// ║  5. ВСПОМОГАТЕЛЬНЫЕ ТИПЫ И ФУНКЦИИ                    ║
// ╚══════════════════════════════════════════════════════════╝

// responseWriter — обёртка для захвата статус-кода
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// timeoutWriter — отслеживает таймаут
type timeoutWriter struct {
    http.ResponseWriter
    timeout bool
}

func (tw *timeoutWriter) WriteHeader(code int) {
    if !tw.timeout {
        tw.ResponseWriter.WriteHeader(code)
    }
}

func (tw *timeoutWriter) Write(b []byte) (int, error) {
    if tw.timeout {
        return 0, nil
    }
    return tw.ResponseWriter.Write(b)
}

// GetRequestID — извлекает request ID из контекста
func GetRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(requestIDKey).(string); ok {
        return id
    }
    return "unknown"
}

// writeJSON — хелпер для JSON-ответов
func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

// ╔══════════════════════════════════════════════════════════╗
// ║  6. ВАЛИДАЦИЯ С go-playground/validator                ║
// ╚══════════════════════════════════════════════════════════╝

var validate = validator.New()

// CreateUserRequest — пример DTO с тегами валидации
type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"gte=0,lte=150"`
    Role  string `json:"role" validate:"required,oneof=admin user moderator"`
}

// FormatValidationErrors — преобразует ошибки валидатора в читаемый формат
func FormatValidationErrors(err error) []ValidationError {
    var validationErrors []ValidationError

    if ve, ok := err.(validator.ValidationErrors); ok {
        for _, fe := range ve {
            validationErrors = append(validationErrors, ValidationError{
                Field:   fe.Field(),
                Message: fmt.Sprintf("validation failed on '%s' tag", fe.Tag()),
            })
        }
    }
    return validationErrors
}

// ╔══════════════════════════════════════════════════════════╗
// ║  7. ХЕНДЛЕРЫ                                           ║
// ╚══════════════════════════════════════════════════════════╝

func helloHandler(w http.ResponseWriter, r *http.Request) {
    reqID := GetRequestID(r.Context())
    writeJSON(w, http.StatusOK, map[string]any{
        "message":    "Hello, Gopher!",
        "request_id": reqID,
    })
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeJSON(w, http.StatusBadRequest, ErrorResponse{
            Error:   "bad_request",
            Message: "Invalid JSON format",
        })
        return
    }

    // Валидация
    if err := validate.Struct(req); err != nil {
        writeJSON(w, http.StatusUnprocessableEntity, ValidationErrorResponse{
            Error:  "validation_error",
            Fields: FormatValidationErrors(err),
        })
        return
    }

    writeJSON(w, http.StatusCreated, map[string]any{
        "message": "User created successfully",
        "user":    req,
    })
}

func panicHandler(w http.ResponseWriter, r *http.Request) {
    // Эта паника будет поймана RecoveryMiddleware
    panic("something went terribly wrong!")
}

func slowHandler(w http.ResponseWriter, r *http.Request) {
    // Демонстрация таймаута (если timeout < 2 секунд)
    time.Sleep(2 * time.Second)
    writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

// ╔══════════════════════════════════════════════════════════╗
// ║  8. НАСТРОЙКА СЕРВЕРА                                  ║
// ╚══════════════════════════════════════════════════════════╝

func setupRouter() http.Handler {
    mux := http.NewServeMux()

    // Публичные эндпоинты
    mux.HandleFunc("GET /api/hello", helloHandler)
    mux.HandleFunc("POST /api/users", createUserHandler)

    // Демонстрационные эндпоинты
    mux.HandleFunc("GET /api/panic", panicHandler)   // Демонстрация recovery
    mux.HandleFunc("GET /api/slow", slowHandler)     // Демонстрация timeout

    // Применяем middleware (порядок ВАЖЕН!)
    rateLimiter := NewRateLimiter(10, 20) // 10 запросов/сек, burst 20

    handler := Chain(
        RecoveryMiddleware,           // 1. Ловим паники (самый внешний)
        RequestIDMiddleware,          // 2. Генерируем request ID
        LoggingMiddleware,            // 3. Логируем
        SecurityHeadersMiddleware,    // 4. Заголовки безопасности
        CORSMiddleware(               // 5. CORS
            []string{"http://localhost:3000", "http://localhost:8080"},
            []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
            []string{"Content-Type", "Authorization", "X-Request-ID"},
        ),
        RateLimitMiddleware(rateLimiter), // 6. Rate limiting
        TimeoutMiddleware(5*time.Second), // 7. Таймаут запроса (самый внутренний)
    )(mux)

    return handler
}

// ╔══════════════════════════════════════════════════════════╗
// ║  MAIN                                                    ║
// ╚══════════════════════════════════════════════════════════╝

func main() {
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    log.Println("🚀 Запуск сервера с middleware...")

    handler := setupRouter()

    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Запуск
    go func() {
        log.Println("Сервер слушает на http://localhost:8080")
        log.Println("Эндпоинты:")
        log.Println("  GET  /api/hello")
        log.Println("  POST /api/users")
        log.Println("  GET  /api/panic  (демонстрация recovery)")
        log.Println("  GET  /api/slow   (демонстрация timeout)")

        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Ошибка сервера: %v", err)
        }
    }()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("🛑 Выключение сервера...")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Ошибка при остановке: %v", err)
    }
    log.Println("✅ Сервер остановлен")
}

🧪 Тестирование

# Запуск сервера
go run main.go

# В другом терминале:

# 1. Обычный запрос (с request ID)
curl -v http://localhost:8080/api/hello

# 2. Создание пользователя (с валидацией)
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com","age":30,"role":"admin"}'

# 3. Ошибка валидации
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"A","email":"invalid","age":200,"role":"super"}'

# 4. CORS preflight
curl -X OPTIONS http://localhost:8080/api/hello \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: GET" \
  -v

# 5. Проверка rate limiting (быстро отправить много запросов)
for i in {1..25}; do
  curl -s http://localhost:8080/api/hello &
done
wait

# 6. Проверка recovery (паника)
curl http://localhost:8080/api/panic

# 7. Проверка таймаута
curl http://localhost:8080/api/slow

📊 Порядок middleware (важен!)

ПозицияMiddlewareНазначение
1 (внешний)RecoveryЛовит паники из ВСЕХ middleware и хендлеров
2RequestIDГенерирует ID для логирования
3LoggingЛогирует запрос и ответ
4SecurityHeadersДобавляет защитные заголовки
5CORSОбрабатывает preflight и добавляет CORS-заголовки
6RateLimitОтклоняет запросы сверх лимита
7 (внутренний)TimeoutОграничивает время выполнения запроса
HandlerБизнес-логика

🚀 Запуск программы

go run main.go

# Сборка
go build -o middleware-demo main.go
./middleware-demo
⚠️ Типичные ошибки:
💡 Практический совет:

💡 Best practices от сеньоров:

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →