Перейти к основному содержимому

Middleware Routing Guide

Это руководство объясняет, как применять разные middleware к разным маршрутам в Tonica gateway.

Проблема

При использовании gRPC gateway с proto аннотациями, все HTTP маршруты регистрируются через один wildcard route. Это означает, что все middleware применяются глобально ко всем маршрутам.

Техническое решение

Tonica использует комбинацию Gin Route Groups для специфичных префиксов и NoRoute handler для всех остальных маршрутов.

NoRoute() - это специальный handler в Gin, который вызывается только когда ни один другой маршрут не совпал. Это позволяет избежать конфликтов между wildcard маршрутами и конкретными путями (например, /openapi.json, /docs).

Порядок обработки запросов:

  1. Специальные маршруты (/openapi.json, /docs, /healthz, etc.)
  2. Route groups с middleware для специфичных префиксов (/api, /public, etc.)
  3. NoRoute handler - catch-all для всех остальных маршрутов

Часто требуется:

  • Публичные маршруты без аутентификации (/public/*)
  • API маршруты с JWT аутентификацией (/api/v1/*)
  • Внутренние маршруты с API key аутентификацией (/internal/*)
  • Админские маршруты с дополнительной проверкой ролей (/admin/*)

Решение 1: Route Groups (Рекомендуется)

Самый чистый и производительный подход - использовать Gin route groups с разными middleware.

Преимущества:

  • ✅ Чистый и понятный код
  • ✅ Хорошая производительность (Gin routing)
  • ✅ Легко тестировать
  • ✅ Декларативная конфигурация

Недостатки:

  • ❌ Порядок регистрации маршрутов важен
  • ❌ Более сложная конфигурация для динамических правил

Использование:

app := tonica.NewApp(
tonica.WithName("my-app"),

// Публичные маршруты - без аутентификации
tonica.WithRouteMiddleware(
[]string{"/public", "/health"},
loggingMiddleware(),
),

// API v1 - JWT аутентификация
tonica.WithRouteMiddleware(
[]string{"/api/v1"},
jwtAuthMiddleware(),
identity.Middleware(identity.JWTExtractor("jwt_claims", "user_id", "email", "role")),
rateLimitMiddleware(),
),

// Internal API - API key
tonica.WithRouteMiddleware(
[]string{"/internal"},
apiKeyAuthMiddleware(),
),

// Admin - JWT + роль admin
tonica.WithRouteMiddleware(
[]string{"/admin"},
jwtAuthMiddleware(),
identity.Middleware(identity.JWTExtractor("jwt_claims", "user_id", "email", "role")),
adminOnlyMiddleware(),
),
)

Proto файл пример:

service UserService {
// Публичный маршрут - без auth
rpc GetPublicInfo(GetPublicInfoRequest) returns (GetPublicInfoResponse) {
option (google.api.http) = {
get: "/public/info"
};
}

// API v1 - требует JWT
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/api/v1/users/{id}"
};
}

// Internal - требует API key
rpc InternalSync(InternalSyncRequest) returns (InternalSyncResponse) {
option (google.api.http) = {
post: "/internal/sync"
};
}

// Admin - требует JWT + admin роль
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {
option (google.api.http) = {
delete: "/admin/users/{id}"
};
}
}

Решение 2: Conditional Middleware

Альтернативный подход - один middleware, который проверяет путь и применяет нужную логику.

Преимущества:

  • ✅ Вся логика в одном месте
  • ✅ Легко добавлять динамические правила
  • ✅ Проще для простых кейсов

Недостатки:

  • ❌ Хуже производительность (проверка всех правил на каждый запрос)
  • ❌ Сложнее тестировать
  • ❌ Императивный подход

Использование:

// В app.go или отдельном файле
func setupConditionalMiddleware() gin.HandlerFunc {
cm := tonica.NewConditionalMiddleware()

// Публичные маршруты
cm.AddRule(
[]string{"/public", "/health"},
loggingMiddleware(),
)

// API v1
cm.AddRule(
[]string{"/api/v1"},
jwtAuthMiddleware(),
identityMiddleware(),
rateLimitMiddleware(),
)

// Internal
cm.AddRule(
[]string{"/internal"},
apiKeyAuthMiddleware(),
)

// Admin
cm.AddRule(
[]string{"/admin"},
jwtAuthMiddleware(),
identityMiddleware(),
adminOnlyMiddleware(),
)

return cm.Handler()
}

// Затем в registerAPI:
router.Use(setupConditionalMiddleware())
router.Any("/*any", WrapH(a.registerGateway(ctx)))

Решение 3: Path-specific Middleware в Handler

Самый простой подход для простых случаев - проверка пути внутри middleware.

Использование:

func smartAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path

// Публичные маршруты - пропускаем
if strings.HasPrefix(path, "/public") || strings.HasPrefix(path, "/health") {
c.Next()
return
}

// Internal - проверяем API key
if strings.HasPrefix(path, "/internal") {
apiKey := c.GetHeader("X-API-Key")
if apiKey == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "api key required"})
return
}
// validate API key...
c.Next()
return
}

// Все остальные - требуют JWT
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "auth required"})
return
}
// validate JWT...

// Admin маршруты - дополнительная проверка роли
if strings.HasPrefix(path, "/admin") {
// check admin role...
}

c.Next()
}
}

// В registerAPI:
router.Use(smartAuthMiddleware())
router.Any("/*any", WrapH(a.registerGateway(ctx)))

Примеры Middleware

JWT Authentication

func jwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "missing authorization header"})
return
}

// Parse and validate JWT
claims, err := validateJWT(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "invalid token"})
return
}

c.Set("jwt_claims", claims)
c.Next()
}
}

API Key Authentication

func apiKeyAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("X-API-Key")
if apiKey == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "missing api key"})
return
}

// Validate API key
if !isValidAPIKey(apiKey) {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "invalid api key"})
return
}

c.Next()
}
}

Role Check Middleware

func requireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
identity, exists := c.Get("identity")
if !exists {
c.AbortWithStatusJSON(http.StatusForbidden,
gin.H{"error": "identity not found"})
return
}

id, ok := identity.(identity.Identity)
if !ok || id.GetRole() != role {
c.AbortWithStatusJSON(http.StatusForbidden,
gin.H{"error": fmt.Sprintf("%s access required", role)})
return
}

c.Next()
}
}

// Использование:
tonica.WithRouteMiddleware(
[]string{"/admin"},
jwtAuthMiddleware(),
identity.Middleware(identity.JWTExtractor(...)),
requireRole("admin"),
)

Рекомендации

  1. Для production приложений - используйте Route Groups (Решение 1)
  2. Для простых случаев - используйте Path-specific Middleware (Решение 3)
  3. Для динамических правил - используйте Conditional Middleware (Решение 2)

Порядок выполнения

При использовании Route Groups, middleware выполняются в следующем порядке:

  1. Глобальные middleware (из router.Use())
  2. Route group middleware (из WithRouteMiddleware())
  3. Handler (gateway)

Пример:

router.Use(globalLogging(), globalCORS())  // 1. Выполнятся всегда

tonica.WithRouteMiddleware(
[]string{"/api/v1"},
jwtAuth(), // 2. Только для /api/v1/*
rateLimit(), // 3. Только для /api/v1/*
)

// 4. Gateway handler

Полный пример

См. examples/router_groups/middleware_routes_example.go для полного рабочего примера.