Files
Authorization/middleware/rate_limiter.go
T
admin ae1831e61f feat: standardize field names and add flexible role_id handling for JWT compatibility
- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
  - Support both user_id/users_id and email/email_address field names
  - Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
  - Entry/exit logs with full context
  - Success (✓) and failure (✗) indicators
  - Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens

This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
2026-02-03 16:35:16 +08:00

100 lines
2.5 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) {
// Fail-open: Skip rate limiting if Redis is not available (prevents full outage)
if redisclient.RDB == nil {
helper.LogError(nil, "Rate limiter: Redis not available, allowing request (fail-open)")
next.ServeHTTP(w, r)
return
}
// Extract user identifier (prefer users_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
}