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
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:
parent
134ab5b2f8
commit
4779071fcd
32
.env.example
Normal file
32
.env.example
Normal 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
8
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
||||
@ -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
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
76
internal/transport/http/middleware/middleware.go
Normal file
76
internal/transport/http/middleware/middleware.go
Normal 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))
|
||||
}
|
||||
70
internal/transport/http/middleware/middleware_test.go
Normal file
70
internal/transport/http/middleware/middleware_test.go
Normal 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()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user