Chuyển đến nội dung chính

Lesson 12: Authorization, Security & Middleware

RBAC with Casbin, permission models. CORS configuration, rate limiting, security headers. Input validation, SQL injection prevention, XSS protection. Custom middleware chains.

💻 Programming — Lesson 12 Lesson 12: Authorization, Security & Middleware

Golang: From Basics to Advanced

Part 3: Database & Authentication

xdev.asia

1. Role-Based Access Control (RBAC) with Casbin

1.1. Setup

go get github.com/casbin/casbin/v2
go get github.com/casbin/gorm-adapter/v3

1.2. Model Configuration

# config/rbac_model.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act
  • sub: subject (user role)
  • obj: object (API path)
  • act. act: action (HTTP method)

1.3. Policy Rules

# config/rbac_policy.csv
p, admin,  /api/*,         GET
p, admin,  /api/*,         POST
p, admin,  /api/*,         PUT
p, admin,  /api/*,         DELETE
p, admin,  /admin/*,       GET
p, admin,  /admin/*,       POST

p, editor, /api/posts,     GET
p, editor, /api/posts,     POST
p, editor, /api/posts/:id, PUT
p, editor, /api/posts/:id, DELETE

p, user,   /api/posts,     GET
p, user,   /api/profile,   GET
p, user,   /api/profile,   PUT

g, admin, editor
g, editor, user

1.4. Casbin Middleware

package middleware

import (
    "net/http"
    
    "github.com/casbin/casbin/v2"
    "github.com/gin-gonic/gin"
)

func CasbinMiddleware(enforcer *casbin.Enforcer) gin.HandlerFunc {
    return func(c *gin.Context) {
        role := GetUserRole(c)
        path := c.Request.URL.Path
        method := c.Request.Method
        
        allowed, err := enforcer.Enforce(role, path, method)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "error": "Authorization error",
            })
            return
        }
        
        if !allowed {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
                "error": "Access denied",
            })
            return
        }
        
        c.Next()
    }
}

// Setup enforcer
func SetupCasbin() (*casbin.Enforcer, error) {
    enforcer, err := casbin.NewEnforcer("config/rbac_model.conf", "config/rbac_policy.csv")
    if err != nil {
        return nil, err
    }
    
    // Load policies
    if err := enforcer.LoadPolicy(); err != nil {
        return nil, err
    }
    
    return enforcer, nil
}

2. CORS Configuration

go get github.com/gin-contrib/cors
import "github.com/gin-contrib/cors"

func SetupCORS(r *gin.Engine) {
    config := cors.Config{
        AllowOrigins:     []string{"https://example.com", "https://admin.example.com"},
        AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
        ExposeHeaders:    []string{"Content-Length"},
        AllowCredentials: true,
        MaxAge:           12 * time.Hour, // Preflight cache
    }
    
    r.Use(cors.New(config))
}

// ⚠️ KHÔNG dùng AllowAllOrigins: true trong production
// ⚠️ KHÔNG dùng AllowOrigins: []string{"*"} khi AllowCredentials: true

3. Rate Limiting

package middleware

import (
    "net/http"
    "sync"
    "time"
    
    "github.com/gin-gonic/gin"
    "golang.org/x/time/rate"
)

type IPRateLimiter struct {
    mu       sync.RWMutex
    limiters map[string]*rate.Limiter
    rate     rate.Limit
    burst    int
}

func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
    return &IPRateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rate:     r,
        burst:    burst,
    }
}

func (rl *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    rl.mu.Lock()
    defer rl.mu.Unlock()
    
    limiter, exists := rl.limiters[ip]
    if !exists {
        limiter = rate.NewLimiter(rl.rate, rl.burst)
        rl.limiters[ip] = limiter
    }
    
    return limiter
}

func RateLimitMiddleware(rps float64, burst int) gin.HandlerFunc {
    limiter := NewIPRateLimiter(rate.Limit(rps), burst)
    
    // Cleanup old entries periodically
    go func() {
        for range time.Tick(10 * time.Minute) {
            limiter.mu.Lock()
            limiter.limiters = make(map[string]*rate.Limiter)
            limiter.mu.Unlock()
        }
    }()
    
    return func(c *gin.Context) {
        ip := c.ClientIP()
        l := limiter.GetLimiter(ip)
        
        if !l.Allow() {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error": "Too many requests. Please try again later.",
            })
            return
        }
        
        c.Next()
    }
}

// Usage: r.Use(RateLimitMiddleware(10, 20)) // 10 req/s, burst 20

4. Security Headers

func SecurityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("X-Content-Type-Options", "nosniff")
        c.Header("X-Frame-Options", "DENY")
        c.Header("X-XSS-Protection", "1; mode=block")
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        c.Header("Content-Security-Policy", "default-src 'self'")
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
        c.Header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
        
        c.Next()
    }
}

5. Input Validation

package validator

import (
    "regexp"
    
    "github.com/go-playground/validator/v10"
)

// Custom validators
func RegisterCustomValidators(v *validator.Validate) {
    // Username: chữ, số, gạch dưới, 3-30 ký tự
    v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
        re := regexp.MustCompile(`^[a-zA-Z0-9_]{3,30}$`)
        return re.MatchString(fl.Field().String())
    })
    
    // No HTML tags (XSS prevention)
    v.RegisterValidation("noscript", func(fl validator.FieldLevel) bool {
        re := regexp.MustCompile(`<[^>]*>`)
        return !re.MatchString(fl.Field().String())
    })
    
    // Vietnamese phone number
    v.RegisterValidation("vnphone", func(fl validator.FieldLevel) bool {
        re := regexp.MustCompile(`^(\+84|0)(3|5|7|8|9)\d{8}$`)
        return re.MatchString(fl.Field().String())
    })
}

// Input structs with validation
type CreatePostInput struct {
    Title   string   `json:"title"   binding:"required,min=5,max=200,noscript"`
    Content string   `json:"content" binding:"required,min=50"`
    Tags    []string `json:"tags"    binding:"max=10,dive,min=1,max=50"`
}

6. SQL Injection Prevention

// ❌ NGUY HIỂM - SQL Injection
db.Raw("SELECT * FROM users WHERE email = '" + email + "'")

// ✅ AN TOÀN - Parameterized queries
db.Raw("SELECT * FROM users WHERE email = ?", email)

// ✅ AN TOÀN - GORM methods
db.Where("email = ?", email).First(&user)

// ✅ AN TOÀN - Named parameters
db.Where("email = @email AND role = @role", map[string]interface{}{
    "email": email, "role": role,
}).First(&user)

7. Complete Middleware Chain

func SetupRouter(tokenService *auth.TokenService, enforcer *casbin.Enforcer) *gin.Engine {
    r := gin.New()
    
    // Global middleware (thứ tự quan trọng!)
    r.Use(gin.Recovery())                      // 1. Recover panics
    r.Use(SecurityHeadersMiddleware())         // 2. Security headers
    r.Use(SetupCORS(r))                        // 3. CORS
    r.Use(RequestIDMiddleware())               // 4. Request ID
    r.Use(LoggingMiddleware())                 // 5. Structured logging
    r.Use(RateLimitMiddleware(100, 200))       // 6. Rate limit
    
    // Public routes
    public := r.Group("/auth")
    public.Use(RateLimitMiddleware(5, 10))     // Stricter rate limit for auth
    {
        public.POST("/login", authHandler.Login)
        public.POST("/register", authHandler.Register)
        public.POST("/refresh", authHandler.RefreshToken)
    }
    
    // Protected routes
    api := r.Group("/api")
    api.Use(AuthMiddleware(tokenService))       // Auth required
    api.Use(CasbinMiddleware(enforcer))         // RBAC check
    {
        api.GET("/profile", getProfile)
        api.PUT("/profile", updateProfile)
        api.GET("/posts", listPosts)
        api.POST("/posts", createPost)
    }
    
    return r
}

8. Request ID Middleware

import "github.com/google/uuid"

func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := c.GetHeader("X-Request-ID")
        if requestID == "" {
            requestID = uuid.New().String()
        }
        
        c.Set("request_id", requestID)
        c.Header("X-Request-ID", requestID)
        c.Next()
    }
}

9. Summary

LayerPurposeTools
AuthenticationUser authenticationJWT + bcrypt
AuthorizationDecentralizationRBAC Casbin
Rate LimitingAnti-DDoS/brute forcegolang.org/x/time/rate
Input ValidationValidate & sanitize inputgo-playground/validator
SQL PreventionPrevent SQL injectionGORM parameterized queries
HeadersSecurity headersCustom middleware
CORSCross-origin controlgin-contrib/cors

Next article: WebSockets & Real-time Communication — gorilla/websocket, chat rooms, and broadcast patterns.