Files
Authorization/middleware/rate_limiter.go
T
admin 0d8f5b9600 feat: implement horizontal scaling optimizations for authz service
- Add /health and /ready endpoints for load balancer health checks
- Replace in-memory JWT token cache with Redis for multi-replica support
- Reduce DB connection pool from 100 to 25 connections per replica
- Add distributed rate limiting (100 req/min + 20 burst) using Redis
- Implement circuit breakers for DB and Redis to prevent cascading failures

This enables the service to scale horizontally with multiple replicas
behind a load balancer without exhausting database connections or
maintaining separate token caches per instance.
2025-12-16 10:03:18 +08:00

99 lines
2.4 KiB
Go

package middleware
import (
"authorization/helper"
"authorization/models"
"authorization/redisclient"
"context"
"fmt"
"net/http"
"time"
)
// DefaultRateLimitConfig returns default rate limiting settings
func DefaultRateLimitConfig() models.RateLimitConfig {
return models.RateLimitConfig{
RequestsPerMinute: 100,
BurstSize: 20,
}
}
// RateLimiterMiddleware implements distributed rate limiting using Redis
func RateLimiterMiddleware(config models.RateLimitConfig) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Skip rate limiting if Redis is not available
if redisclient.RDB == nil {
helper.RespondWithError(w, http.StatusServiceUnavailable, "Redis not available")
return
}
// Extract user identifier (prefer user_id from JWT, fallback to IP)
var identifier string
if userID, ok := GetUserID(r); ok {
identifier = "user:" + userID
} else {
identifier = "ip:" + getClientIP(r)
}
// Check rate limit
allowed, err := checkRateLimit(identifier, config)
if err != nil {
// On error, fail open (allow request) but log the error
helper.LogError(err, "rate limiter error")
next.ServeHTTP(w, r)
return
}
if !allowed {
helper.RespondWithError(w, http.StatusTooManyRequests, "Rate limit exceeded")
return
}
next.ServeHTTP(w, r)
}
}
}
// checkRateLimit uses Redis INCR with sliding window
func checkRateLimit(identifier string, config models.RateLimitConfig) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
key := fmt.Sprintf("ratelimit:%s", identifier)
// Use Redis pipeline for atomic operations
pipe := redisclient.RDB.Pipeline()
incrCmd := pipe.Incr(ctx, key)
pipe.Expire(ctx, key, time.Minute)
_, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
count := incrCmd.Val()
// Allow burst + requests per minute
return count <= int64(config.RequestsPerMinute+config.BurstSize), nil
}
// getClientIP extracts the client IP from the request
func getClientIP(r *http.Request) string {
// Check X-Forwarded-For header first (for proxies/load balancers)
forwarded := r.Header.Get("X-Forwarded-For")
if forwarded != "" {
return forwarded
}
// Check X-Real-IP header
realIP := r.Header.Get("X-Real-IP")
if realIP != "" {
return realIP
}
// Fallback to RemoteAddr
return r.RemoteAddr
}