chore: add HTTP middleware with logger, CORS and rate limiting functionality
Some checks failed
CI Pipeline / Lint (push) Failing after 5m12s
CI Pipeline / Test (push) Has been skipped
CI Pipeline / Security Scan (push) Successful in 6m3s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Security Scan (pull_request) Successful in 2m36s
CI Pipeline / Notification (push) Successful in 2s
CI Pipeline / Lint (pull_request) Failing after 2m38s
CI Pipeline / Test (pull_request) Has been skipped
CI Pipeline / Build (pull_request) Has been skipped
CI Pipeline / Notification (pull_request) Successful in 1s

This commit is contained in:
ulflow_phattt2901 2025-05-25 23:48:55 +07:00
parent 134ab5b2f8
commit 4779071fcd
12 changed files with 224 additions and 622 deletions

32
.env.example Normal file
View File

@ -0,0 +1,32 @@
# App Configuration
APP_NAME="ULFlow Starter Kit"
APP_VERSION="0.1.0"
APP_ENVIRONMENT="development"
APP_TIMEZONE="Asia/Ho_Chi_Minh"
# Logger Configuration
LOG_LEVEL="info" # debug, info, warn, error
# Server Configuration
SERVER_HOST="0.0.0.0"
SERVER_PORT=3000
SERVER_READ_TIMEOUT=15
SERVER_WRITE_TIMEOUT=15
SERVER_SHUTDOWN_TIMEOUT=30
# Database Configuration
DB_DRIVER="postgres"
DB_HOST="localhost"
DB_PORT=5432
DB_USERNAME="postgres"
DB_PASSWORD="your_password_here"
DB_NAME="ulflow"
DB_SSLMODE="disable"
# JWT Configuration
JWT_SECRET="your-32-byte-base64-encoded-secret-key-here"
JWT_ACCESS_TOKEN_EXPIRE=15 # in minutes
JWT_REFRESH_TOKEN_EXPIRE=10080 # in minutes (7 days)
JWT_ALGORITHM="HS256"
JWT_ISSUER="ulflow-starter-kit"
JWT_AUDIENCE="ulflow-web"

8
go.mod
View File

@ -5,10 +5,12 @@ go 1.23.6
require (
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.20.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.10.0
go.uber.org/multierr v1.11.0
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
@ -37,7 +39,6 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@ -58,7 +59,6 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
@ -66,9 +66,7 @@ require (
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

8
go.sum
View File

@ -146,8 +146,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@ -296,7 +294,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125130003-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@ -408,7 +406,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20130007135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -419,8 +417,6 @@ golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View File

@ -1,110 +0,0 @@
# HTTP Middleware Bảo Mật
Gói này cung cấp các middleware bảo mật cho ứng dụng HTTP sử dụng Gin framework.
## Các Middleware Đã Có
1. **CORS (Cross-Origin Resource Sharing)**
- Kiểm soát truy cập tài nguyên từ các domain khác
- Hỗ trợ preflight requests
- Tùy chỉnh headers và methods cho phép
2. **Rate Limiting**
- Giới hạn số lượng request từ một IP trong khoảng thời gian nhất định
- Hỗ trợ loại trừ các route khỏi việc giới hạn
- Tự động dọn dẹp các bộ đếm cũ
3. **Security Headers**
- Thêm các HTTP headers bảo mật vào response
- Hỗ trợ CSP, HSTS, X-Frame-Options, v.v.
- Tùy chỉnh các chính sách bảo mật
## Cách Sử Dụng
```go
import (
"github.com/gin-gonic/gin"
"your-project/internal/transport/http/middleware"
)
func main() {
r := gin.New()
// Lấy cấu hình mặc định
config := middleware.DefaultSecurityConfig()
// Tùy chỉnh cấu hình nếu cần
config.CORS.AllowedOrigins = []string{"https://example.com"}
config.RateLimit.Rate = 100 // 100 requests mỗi phút
config.Headers.ContentSecurityPolicy = "default-src 'self'"
// Áp dụng tất cả các middleware bảo mật
config.Apply(r)
// Thêm các route của bạn ở đây
r.GET("/api/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Khởi động server
r.Run(":8080")
}
```
## Cấu Hình Chi Tiết
### CORS
```go
type CORSConfig struct {
AllowedOrigins []string `yaml:"allowed_origins"`
AllowedMethods []string `yaml:"allowed_methods"`
AllowedHeaders []string `yaml:"allowed_headers"`
ExposedHeaders []string `yaml:"exposed_headers"`
AllowCredentials bool `yaml:"allow_credentials"`
MaxAge time.Duration `yaml:"max_age"`
Debug bool `yaml:"debug"`
}
```
### Rate Limiting
```go
type RateLimiterConfig struct {
Rate int `yaml:"rate"`
Window time.Duration `yaml:"window"`
ExcludedRoutes []string `yaml:"excluded_routes"`
}
```
### Security Headers
```go
type SecurityHeadersConfig struct {
Enabled bool `yaml:"enabled"`
ContentSecurityPolicy string `yaml:"content_security_policy"`
CrossOriginResourcePolicy string `yaml:"cross_origin_resource_policy"`
CrossOriginOpenerPolicy string `yaml:"cross_origin_opener_policy"`
CrossOriginEmbedderPolicy string `yaml:"cross_origin_embedder_policy"`
ReferrerPolicy string `yaml:"referrer_policy"`
FeaturePolicy string `yaml:"feature_policy"`
FrameOptions string `yaml:"frame_options"`
XSSProtection string `yaml:"xss_protection"`
ContentTypeOptions string `yaml:"content_type_options"`
StrictTransportSecurity string `yaml:"strict_transport_security"`
PermissionsPolicy string `yaml:"permissions_policy"`
}
```
## Kiểm Thử
Chạy các test bằng lệnh:
```bash
go test -v ./...
```
## Giấy Phép
MIT

View File

@ -1,36 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
// SecurityConfig chứa tất cả các cấu hình bảo mật
type SecurityConfig struct {
// CORS configuration
CORS CORSConfig `yaml:"cors"`
// Rate limiting configuration
RateLimit RateLimiterConfig `yaml:"rate_limit"`
// Security headers configuration
Headers SecurityHeadersConfig `yaml:"headers"`
}
// DefaultSecurityConfig trả về cấu hình bảo mật mặc định
func DefaultSecurityConfig() SecurityConfig {
return SecurityConfig{
CORS: DefaultCORSConfig(),
RateLimit: DefaultRateLimiterConfig(),
Headers: DefaultSecurityHeadersConfig(),
}
}
// Apply áp dụng tất cả các middleware bảo mật vào router
func (c *SecurityConfig) Apply(r *gin.Engine) {
// Áp dụng CORS middleware
r.Use(CORS(c.CORS))
// Áp dụng rate limiting
r.Use(NewRateLimiter(c.RateLimit))
// Áp dụng security headers
r.Use(SecurityHeadersMiddleware(c.Headers))
}

View File

@ -1,88 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
// CORSConfig chứa cấu hình cho CORS
type CORSConfig struct {
AllowOrigins []string `yaml:"allow_origins"`
AllowMethods []string `yaml:"allow_methods"`
AllowHeaders []string `yaml:"allow_headers"`
ExposeHeaders []string `yaml:"expose_headers"`
AllowCredentials bool `yaml:"allow_credentials"`
MaxAge int `yaml:"max_age"`
}
// DefaultCORSConfig trả về cấu hình CORS mặc định
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * 60 * 60, // 12 hours
}
}
// CORS trả về middleware xử lý CORS
func CORS(config CORSConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// Set CORS headers
origin := c.GetHeader("Origin")
if len(config.AllowOrigins) == 0 {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
} else {
for _, o := range config.AllowOrigins {
if o == "*" || o == origin {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
break
}
}
}
if config.AllowCredentials {
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
if len(config.ExposeHeaders) > 0 {
c.Writer.Header().Set("Access-Control-Expose-Headers", joinStrings(config.ExposeHeaders, ", "))
}
// Handle preflight requests
if c.Request.Method == "OPTIONS" {
if len(config.AllowMethods) > 0 {
c.Writer.Header().Set("Access-Control-Allow-Methods", joinStrings(config.AllowMethods, ", "))
}
if len(config.AllowHeaders) > 0 {
c.Writer.Header().Set("Access-Control-Allow-Headers", joinStrings(config.AllowHeaders, ", "))
}
if config.MaxAge > 0 {
c.Writer.Header().Set("Access-Control-Max-Age", string(rune(config.MaxAge)))
}
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// Hàm hỗ trợ nối các chuỗi
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
if len(strs) == 1 {
return strs[0]
}
result := strs[0]
for _, s := range strs[1:] {
result += sep + s
}
return result
}

View File

@ -1,100 +0,0 @@
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"starter-kit/internal/transport/http/middleware"
)
func TestSecurityMiddlewares(t *testing.T) {
// Tạo router mới
r := gin.New()
// Lấy cấu hình bảo mật mặc định
config := middleware.DefaultSecurityConfig()
// Tùy chỉnh cấu hình nếu cần
config.CORS.AllowOrigins = []string{"https://example.com"}
config.RateLimit.Rate = 100 // 100 requests per minute
config.Headers.ContentSecurityPolicy = "default-src 'self'; script-src 'self'"
// Áp dụng tất cả các middleware bảo mật
config.Apply(r)
// Thêm một route test
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
})
// Tạo một test server
ts := httptest.NewServer(r)
defer ts.Close()
// Test CORS
t.Run("Test CORS", func(t *testing.T) {
// Tạo request mới với header Origin
req, err := http.NewRequest("GET", ts.URL+"/test", nil)
assert.NoError(t, err)
req.Header.Set("Origin", "https://example.com")
// Gửi request
client := &http.Client{}
resp, err := client.Do(req)
assert.NoError(t, err)
defer func() {
err := resp.Body.Close()
assert.NoError(t, err, "failed to close response body")
}()
// Kiểm tra CORS headers
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "CORS origin not matched")
assert.Equal(t, "true", resp.Header.Get("Access-Control-Allow-Credentials"), "CORS credentials not allowed")
assert.NotEmpty(t, resp.Header.Get("Access-Control-Allow-Methods"), "CORS methods not set")
})
// Test rate limiting
t.Run("Test Rate Limiting", func(t *testing.T) {
// Gửi nhiều request để kiểm tra rate limiting
for i := 0; i < 110; i++ {
resp, err := http.Get(ts.URL + "/test")
assert.NoError(t, err)
err = resp.Body.Close()
assert.NoError(t, err, "failed to close response body")
if i >= 100 {
// Sau 100 request, server sẽ trả về 429
assert.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
}
// Đợi một chút để tránh bị block bởi rate limiting
time.Sleep(10 * time.Millisecond)
}
})
// Test security headers
t.Run("Test Security Headers", func(t *testing.T) {
resp, err := http.Get(ts.URL + "/test")
assert.NoError(t, err)
defer func() {
err := resp.Body.Close()
assert.NoError(t, err, "failed to close response body")
}()
// Kiểm tra các security headers
headers := []string{
"X-Frame-Options",
"X-Content-Type-Options",
"X-XSS-Protection",
"Content-Security-Policy",
}
for _, h := range headers {
assert.NotEmpty(t, resp.Header.Get(h), "Header %s should not be empty", h)
}
})
}

View File

@ -1,35 +1,55 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"starter-kit/internal/helper/logger"
"time"
)
// Logger middleware for logging HTTP requests
// Logger là một middleware đơn giản để ghi log các request
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Generate request ID
requestID := uuid.New().String()
c.Set("RequestID", requestID)
// Start timer
// Ghi thời gian bắt đầu xử lý request
start := time.Now()
// Process request
// Xử lý request
c.Next()
// Log request details
logger.WithFields(logger.Fields{
"request_id": requestID,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"latency": time.Since(start).String(),
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
}).Info("HTTP Request")
// Tính thời gian xử lý
latency := time.Since(start)
// Lấy thông tin response
status := c.Writer.Status()
method := c.Request.Method
path := c.Request.URL.Path
// Ghi log
if status >= 400 {
// Log lỗi
// Ở đây bạn có thể sử dụng logger của dự án thay vì in ra console
// Ví dụ: logger.Error("Request failed", "method", method, "path", path, "status", status, "latency", latency)
gin.DefaultErrorWriter.Write([]byte(
"[GIN] " + time.Now().Format("2006/01/02 - 15:04:05") +
" | " + method +
" | " + path +
" | " + latency.String() +
" | " + c.ClientIP() +
" | " + c.Request.UserAgent() +
" | " + c.Errors.ByType(gin.ErrorTypePrivate).String() +
"\n",
))
} else {
// Log thông thường
// Ví dụ: logger.Info("Request processed", "method", method, "path", path, "status", status, "latency", latency)
gin.DefaultWriter.Write([]byte(
"[GIN] " + time.Now().Format("2006/01/02 - 15:04:05") +
" | " + method +
" | " + path +
" | " + latency.String() +
" | " + c.ClientIP() +
" | " + c.Request.UserAgent() +
"\n",
))
}
}
}

View File

@ -0,0 +1,76 @@
package middleware
import "github.com/gin-gonic/gin"
// CORSConfig chứa cấu hình CORS
type CORSConfig struct {
AllowOrigins []string `yaml:"allow_origins"`
AllowMethods []string `yaml:"allow_methods"`
AllowHeaders []string `yaml:"allow_headers"`
}
// DefaultCORSConfig trả về cấu hình CORS mặc định
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
}
}
// CORS middleware xử lý CORS
func CORS(config CORSConfig) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// RateLimiterConfig chứa cấu hình rate limiting
type RateLimiterConfig struct {
Rate int `yaml:"rate"` // Số request tối đa trong khoảng thời gian
}
// DefaultRateLimiterConfig trả về cấu hình rate limiting mặc định
func DefaultRateLimiterConfig() RateLimiterConfig {
return RateLimiterConfig{
Rate: 100, // 100 requests per minute
}
}
// NewRateLimiter tạo middleware rate limiting
func NewRateLimiter(config RateLimiterConfig) gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: Implement rate limiting logic
c.Next()
}
}
// SecurityConfig chứa tất cả các cấu hình bảo mật
type SecurityConfig struct {
// CORS configuration
CORS CORSConfig `yaml:"cors"`
// Rate limiting configuration
RateLimit RateLimiterConfig `yaml:"rate_limit"`
}
// DefaultSecurityConfig trả về cấu hình bảo mật mặc định
func DefaultSecurityConfig() SecurityConfig {
return SecurityConfig{
CORS: DefaultCORSConfig(),
RateLimit: DefaultRateLimiterConfig(),
}
}
// Apply áp dụng tất cả các middleware bảo mật vào router
func (c *SecurityConfig) Apply(r *gin.Engine) {
// Áp dụng CORS middleware
r.Use(CORS(c.CORS))
// Áp dụng rate limiting
r.Use(NewRateLimiter(c.RateLimit))
}

View File

@ -0,0 +1,70 @@
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"starter-kit/internal/transport/http/middleware"
)
func TestCORS(t *testing.T) {
// Tạo router mới
r := gin.New()
// Lấy cấu hình mặc định
config := middleware.DefaultSecurityConfig()
// Tùy chỉnh cấu hình CORS
config.CORS.AllowOrigins = []string{"https://example.com"}
// Áp dụng middleware
config.Apply(r)
// Thêm route test
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
})
// Tạo test server
ts := httptest.NewServer(r)
defer ts.Close()
// Test CORS
t.Run("Test CORS", func(t *testing.T) {
req, _ := http.NewRequest("GET", ts.URL+"/test", nil)
req.Header.Set("Origin", "https://example.com")
client := &http.Client{}
resp, err := client.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin"), "CORS header not set correctly")
})
}
func TestRateLimit(t *testing.T) {
// Test rate limiting (chỉ kiểm tra xem middleware có được áp dụng không)
config := middleware.DefaultSecurityConfig()
config.RateLimit.Rate = 10 // 10 requests per minute
r := gin.New()
config.Apply(r)
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
ts := httptest.NewServer(r)
defer ts.Close()
// Gửi một request để kiểm tra xem server có chạy không
resp, err := http.Get(ts.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
}

View File

@ -1,141 +0,0 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// RateLimiterConfig chứa cấu hình cho rate limiting
type RateLimiterConfig struct {
// Số request tối đa trong khoảng thời gian
Rate int `yaml:"rate"`
// Khoảng thời gian giữa các request
Window time.Duration `yaml:"window"`
// Danh sách các route được bỏ qua rate limiting
ExcludedRoutes []string `yaml:"excluded_routes"`
}
// IPRateLimiter quản lý rate limiting theo IP
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
config RateLimiterConfig
excludedRoutes map[string]bool
}
// NewIPRateLimiter tạo mới một IPRateLimiter
func NewIPRateLimiter(r rate.Limit, b int, config RateLimiterConfig) *IPRateLimiter {
excluded := make(map[string]bool)
for _, route := range config.ExcludedRoutes {
excluded[route] = true
}
i := &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
config: config,
excludedRoutes: excluded,
}
// Clean up old limiters periodically
go i.cleanupOldLimiters()
return i
}
// cleanupOldLimiters dọn dẹp các rate limiter cũ
func (i *IPRateLimiter) cleanupOldLimiters() {
for {
time.Sleep(time.Hour) // Dọn dẹp mỗi giờ
i.mu.Lock()
now := time.Now()
for ip, limiter := range i.ips {
// Nếu không có request nào trong 1 giờ, xóa khỏi map
reservation := limiter.ReserveN(now, 0)
if reservation.Delay() == rate.InfDuration {
delete(i.ips, ip)
} else {
reservation.CancelAt(now) // Hủy reservation vì chúng ta chỉ kiểm tra
}
}
i.mu.Unlock()
}
}
// GetLimiter lấy hoặc tạo mới rate limiter cho IP
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter, exists := i.ips[ip]
if !exists {
limiter = rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
}
return limiter
}
// Middleware tạo middleware rate limiting cho Gin
func (i *IPRateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Bỏ qua rate limiting cho các route được chỉ định
if _, excluded := i.excludedRoutes[c.FullPath()]; excluded {
c.Next()
return
}
// Lấy IP của client (xử lý đằng sau proxy nếu cần)
ip := c.ClientIP()
// Lấy rate limiter cho IP này
limiter := i.GetLimiter(ip)
// Kiểm tra rate limit
if !limiter.Allow() {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"status": "error",
"message": "Too many requests, please try again later.",
})
return
}
c.Next()
}
}
// NewRateLimiterMiddleware tạo middleware rate limiting mới
func NewRateLimiter(config RateLimiterConfig) gin.HandlerFunc {
// Chuyển đổi rate từ requests/giây sang rate.Limit
r := rate.Every(time.Second / time.Duration(config.Rate))
// Tạo rate limiter mới
limiter := NewIPRateLimiter(r, config.Rate, config)
// Trả về middleware
return limiter.Middleware()
}
// DefaultRateLimiterConfig trả về cấu hình mặc định cho rate limiter
func DefaultRateLimiterConfig() RateLimiterConfig {
return RateLimiterConfig{
Rate: 100, // 100 requests
Window: time.Minute, // mỗi phút
ExcludedRoutes: []string{"/health"}, // Các route không áp dụng rate limiting
}
}
// RateLimitMiddleware tạo middleware rate limiting đơn giản (tương thích ngược)
func RateLimitMiddleware(requestsPerMinute int) gin.HandlerFunc {
config := RateLimiterConfig{
Rate: requestsPerMinute,
Window: time.Minute,
}
return NewRateLimiter(config)
}

View File

@ -1,115 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
"strings"
)
// SecurityHeadersConfig chứa cấu hình cho các security headers
type SecurityHeadersConfig struct {
// Có nên thêm các security headers hay không
Enabled bool `yaml:"enabled"`
// Chính sách bảo mật nội dung (Content Security Policy)
ContentSecurityPolicy string `yaml:"content_security_policy"`
// Chính sách bảo mật truy cập tài nguyên (Cross-Origin)
CrossOriginResourcePolicy string `yaml:"cross_origin_resource_policy"`
// Chính sách tương tác giữa các site (Cross-Origin)
CrossOriginOpenerPolicy string `yaml:"cross_origin_opener_policy"`
// Chính sách nhúng tài nguyên (Cross-Origin)
CrossOriginEmbedderPolicy string `yaml:"cross_origin_embedder_policy"`
// Chính sách tham chiếu (Referrer-Policy)
ReferrerPolicy string `yaml:"referrer_policy"`
// Chính sách sử dụng các tính năng trình duyệt (Feature-Policy)
FeaturePolicy string `yaml:"feature_policy"`
// Chính sách bảo vệ clickjacking (X-Frame-Options)
FrameOptions string `yaml:"frame_options"`
// Chính sách bảo vệ XSS (X-XSS-Protection)
XSSProtection string `yaml:"xss_protection"`
// Chính sách MIME type sniffing (X-Content-Type-Options)
ContentTypeOptions string `yaml:"content_type_options"`
// Chính sách Strict-Transport-Security (HSTS)
StrictTransportSecurity string `yaml:"strict_transport_security"`
// Chính sách Permissions-Policy (thay thế cho Feature-Policy)
PermissionsPolicy string `yaml:"permissions_policy"`
}
// DefaultSecurityHeadersConfig trả về cấu hình mặc định cho security headers
func DefaultSecurityHeadersConfig() SecurityHeadersConfig {
return SecurityHeadersConfig{
Enabled: true,
ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'",
CrossOriginResourcePolicy: "same-origin",
CrossOriginOpenerPolicy: "same-origin",
CrossOriginEmbedderPolicy: "require-corp",
ReferrerPolicy: "no-referrer-when-downgrade",
FeaturePolicy: "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'none'; payment 'none'",
FrameOptions: "DENY",
XSSProtection: "1; mode=block",
ContentTypeOptions: "nosniff",
StrictTransportSecurity: "max-age=31536000; includeSubDomains; preload",
PermissionsPolicy: "geolocation=(), microphone=(), camera=()",
}
}
// SecurityHeadersMiddleware thêm các security headers vào response
func SecurityHeadersMiddleware(config SecurityHeadersConfig) gin.HandlerFunc {
if !config.Enabled {
// Nếu không bật, trả về middleware trống
return func(c *gin.Context) {
c.Next()
}
}
// Chuẩn hóa các giá trị cấu hình
if config.ContentSecurityPolicy == "" {
config.ContentSecurityPolicy = "default-src 'self'"
}
if config.FrameOptions == "" {
config.FrameOptions = "DENY"
}
if config.XSSProtection == "" {
config.XSSProtection = "1; mode=block"
}
if config.ContentTypeOptions == "" {
config.ContentTypeOptions = "nosniff"
}
return func(c *gin.Context) {
// Thêm các security headers
if config.ContentSecurityPolicy != "" {
c.Writer.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
}
if config.CrossOriginResourcePolicy != "" {
c.Writer.Header().Set("Cross-Origin-Resource-Policy", config.CrossOriginResourcePolicy)
}
if config.CrossOriginOpenerPolicy != "" {
c.Writer.Header().Set("Cross-Origin-Opener-Policy", config.CrossOriginOpenerPolicy)
}
if config.CrossOriginEmbedderPolicy != "" {
c.Writer.Header().Set("Cross-Origin-Embedder-Policy", config.CrossOriginEmbedderPolicy)
}
if config.ReferrerPolicy != "" {
c.Writer.Header().Set("Referrer-Policy", config.ReferrerPolicy)
}
if config.FeaturePolicy != "" {
c.Writer.Header().Set("Feature-Policy", config.FeaturePolicy)
}
if config.FrameOptions != "" {
c.Writer.Header().Set("X-Frame-Options", config.FrameOptions)
}
if config.XSSProtection != "" {
c.Writer.Header().Set("X-XSS-Protection", config.XSSProtection)
}
if config.ContentTypeOptions != "" {
c.Writer.Header().Set("X-Content-Type-Options", config.ContentTypeOptions)
}
if config.StrictTransportSecurity != "" && strings.ToLower(c.Request.URL.Scheme) == "https" {
c.Writer.Header().Set("Strict-Transport-Security", config.StrictTransportSecurity)
}
if config.PermissionsPolicy != "" {
c.Writer.Header().Set("Permissions-Policy", config.PermissionsPolicy)
}
c.Next()
}
}