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 (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.20.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/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
github.com/stretchr/testify v1.10.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/mysql v1.5.7
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlite v1.5.7
|
gorm.io/driver/sqlite v1.5.7
|
||||||
@ -37,7 +39,6 @@ require (
|
|||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // 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/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // 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/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // 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/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.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
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // 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
|
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/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
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.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.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/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=
|
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-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-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-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-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-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/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/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.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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/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-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-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.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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
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
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"time"
|
||||||
"starter-kit/internal/helper/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger middleware for logging HTTP requests
|
// Logger là một middleware đơn giản để ghi log các request
|
||||||
func Logger() gin.HandlerFunc {
|
func Logger() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Generate request ID
|
// Ghi thời gian bắt đầu xử lý request
|
||||||
requestID := uuid.New().String()
|
|
||||||
c.Set("RequestID", requestID)
|
|
||||||
|
|
||||||
// Start timer
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// Process request
|
// Xử lý request
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
// Log request details
|
// Tính thời gian xử lý
|
||||||
logger.WithFields(logger.Fields{
|
latency := time.Since(start)
|
||||||
"request_id": requestID,
|
|
||||||
"method": c.Request.Method,
|
|
||||||
"path": c.Request.URL.Path,
|
// Lấy thông tin response
|
||||||
"status": c.Writer.Status(),
|
status := c.Writer.Status()
|
||||||
"latency": time.Since(start).String(),
|
method := c.Request.Method
|
||||||
"client_ip": c.ClientIP(),
|
path := c.Request.URL.Path
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
}).Info("HTTP Request")
|
// 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