chore: Update config file
Some checks failed
CI Pipeline / Security Scan (push) Failing after 4m42s
CI Pipeline / Lint (push) Failing after 5m1s
CI Pipeline / Test (push) Has been skipped
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Notification (push) Successful in 2s

This commit is contained in:
ulflow_phattt2901 2025-05-25 15:22:23 +07:00
parent e86a866fb6
commit 9df4673657
17 changed files with 1058 additions and 177 deletions

View File

@ -1,5 +1,5 @@
{ {
"enable_database": { "enable_database": {
"enabled": false "enabled": true
} }
} }

View File

@ -29,3 +29,51 @@ database:
max_idle_conns: 5 max_idle_conns: 5
conn_max_lifetime: 300 conn_max_lifetime: 300
migration_path: "migrations" migration_path: "migrations"
<<<<<<< Updated upstream
=======
# JWT Configuration
jwt:
# Generate a secure random secret key using: openssl rand -base64 32
secret: "ulflow2121_this_is_a_secure_key_for_jwt_signing"
# Access Token expiration time in minutes (15 minutes)
access_token_expire: 15
# Refresh Token expiration time in minutes (7 days = 10080 minutes)
refresh_token_expire: 10080
# Algorithm for JWT signing (HS256, HS384, HS512, RS256, etc.)
algorithm: "HS256"
# Issuer for JWT tokens
issuer: "ulflow-starter-kit"
# Audience for JWT tokens
audience: ["ulflow-web"]
# Security configurations
security:
# Rate limiting for authentication endpoints (requests per minute)
rate_limit:
login: 5
register: 3
refresh: 10
# Password policy
password:
min_length: 8
require_upper: true
require_lower: true
require_number: true
require_special: true
# Cookie settings
cookie:
secure: true
http_only: true
same_site: "Lax" # or "Strict" for more security
domain: "" # Set your domain in production
path: "/"
# CORS settings
cors:
allowed_origins: ["*"] # Restrict in production
allowed_methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
allowed_headers: ["Origin", "Content-Type", "Accept", "Authorization"]
exposed_headers: ["Content-Length", "X-Total-Count"]
allow_credentials: true
max_age: 300 # 5 minutes
>>>>>>> Stashed changes

View File

@ -0,0 +1,89 @@
# Cấu hình bảo mật cho ứng dụng
# Cấu hình CORS
cors:
# Danh sách các domain được phép truy cập (sử dụng "*" để cho phép tất cả)
allowed_origins:
- "https://example.com"
- "https://api.example.com"
# Các phương thức HTTP được phép
allowed_methods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
# Các header được phép
allowed_headers:
- Origin
- Content-Type
- Content-Length
- Accept-Encoding
- X-CSRF-Token
- Authorization
- X-Requested-With
- X-Request-ID
# Các header được phép hiển thị
exposed_headers:
- Content-Length
- X-Total-Count
# Cho phép gửi credentials (cookie, authorization headers)
allow_credentials: true
# Thời gian cache preflight request (ví dụ: 5m, 1h)
max_age: 5m
# Bật chế độ debug
debug: false
# Cấu hình Rate Limiting
rate_limit:
# Số request tối đa trong khoảng thời gian
rate: 100
# Khoảng thời gian (ví dụ: 1m, 5m, 1h)
window: 1m
# Danh sách các route được bỏ qua rate limiting
excluded_routes:
- "/health"
- "/metrics"
# Cấu hình Security Headers
headers:
# Bật/tắt security headers
enabled: true
# Chính sách bảo mật nội dung (Content Security Policy)
content_security_policy: "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'"
# Chính sách bảo mật truy cập tài nguyên (Cross-Origin)
cross_origin_resource_policy: "same-origin"
cross_origin_opener_policy: "same-origin"
cross_origin_embedder_policy: "require-corp"
# Chính sách tham chiếu (Referrer-Policy)
referrer_policy: "no-referrer-when-downgrade"
# Chính sách sử dụng các tính năng trình duyệt (Feature-Policy)
feature_policy: "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'"
# Chính sách bảo vệ clickjacking (X-Frame-Options)
frame_options: "DENY"
# Chính sách bảo vệ XSS (X-XSS-Protection)
xss_protection: "1; mode=block"
# Chính sách MIME type sniffing (X-Content-Type-Options)
content_type_options: "nosniff"
# Chính sách Strict-Transport-Security (HSTS)
strict_transport_security: "max-age=31536000; includeSubDomains; preload"
# Chính sách Permissions-Policy (thay thế cho Feature-Policy)
permissions_policy: "geolocation=(), microphone=(), camera=()"

209
docs/AUTHENTICATION.md Normal file
View File

@ -0,0 +1,209 @@
# Hệ thống Xác thực và Phân quyền
## Mục lục
1. [Tổng quan](#tổng-quan)
2. [Luồng xử lý](#luồng-xử-lý)
- [Đăng nhập](#đăng-nhập)
- [Làm mới token](#làm-mới-token)
- [Đăng xuất](#đăng-xuất)
3. [Các thành phần chính](#các-thành-phần-chính)
- [Auth Middleware](#auth-middleware)
- [Token Service](#token-service)
- [Session Management](#session-management)
4. [Bảo mật](#bảo-mật)
5. [Tích hợp](#tích-hợp)
## Tổng quan
Hệ thống xác thực sử dụng JWT (JSON Web Tokens) với cơ chế refresh token để đảm bảo bảo mật. Mỗi phiên đăng nhập sẽ có:
- Access Token: Có thời hạn ngắn (15-30 phút)
- Refresh Token: Có thời hạn dài hơn (7-30 ngày)
- Session ID: Định danh duy nhất cho mỗi phiên
## Luồng xử lý
### Đăng nhập
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant AuthService
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/login
AuthController->>AuthService: Authenticate(credentials)
AuthService->>UserRepository: FindByEmail(email)
AuthService->>PasswordUtil: CompareHashAndPassword()
AuthService->>TokenService: GenerateTokens(userID, sessionID)
TokenService-->>AuthService: tokens
AuthService->>SessionStore: Create(session)
AuthService-->>AuthController: authResponse
AuthController-->>Client: {accessToken, refreshToken, user}
```
### Làm mới Token
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/refresh
AuthController->>TokenService: RefreshToken(refreshToken)
TokenService->>SessionStore: Get(sessionID)
SessionStore-->>TokenService: session
TokenService->>TokenService: ValidateRefreshToken()
TokenService->>SessionStore: UpdateLastUsed()
TokenService-->>AuthController: newTokens
AuthController-->>Client: {accessToken, refreshToken}
```
### Đăng xuất
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant TokenService
participant SessionStore
Client->>AuthController: POST /api/v1/auth/logout
AuthController->>TokenService: ExtractTokenMetadata()
TokenService-->>AuthController: tokenClaims
AuthController->>SessionStore: Delete(sessionID)
AuthController-->>Client: 200 OK
```
## Các thành phần chính
### Auth Middleware
#### `Authenticate()`
- **Mục đích**: Xác thực access token trong header Authorization
- **Luồng xử lý**:
1. Lấy token từ header
2. Xác thực token
3. Kiểm tra session trong store
4. Lưu thông tin user vào context
#### `RequireRole(roles ...string)`
- **Mục đích**: Kiểm tra quyền truy cập dựa trên vai trò
- **Luồng xử lý**:
1. Lấy thông tin user từ context
2. Kiểm tra user có vai trò phù hợp không
3. Trả về lỗi nếu không có quyền
### Token Service
#### `GenerateTokens(userID, sessionID)`
- Tạo access token và refresh token
- Lưu thông tin session
- Trả về cặp token
#### `ValidateToken(token)`
- Xác thực chữ ký token
- Kiểm tra thời hạn
- Trả về claims nếu hợp lệ
### Session Management
#### `CreateSession(session)`
- Tạo session mới
- Lưu vào Redis với TTL
- Trả về session ID
#### `GetSession(sessionID)`
- Lấy thông tin session từ Redis
- Cập nhật thời gian truy cập cuối
- Trả về session nếu tồn tại
## Bảo mật
1. **Token Storage**
- Access Token: Lưu trong memory (không lưu localStorage)
- Refresh Token: HttpOnly, Secure, SameSite=Strict cookie
2. **Token Rotation**
- Mỗi lần refresh sẽ tạo cặp token mới
- Vô hiệu hóa refresh token cũ
3. **Thu hồi token**
- Đăng xuất sẽ xóa session
- Có thể thu hồi tất cả session của user
## Tích hợp
### Frontend
1. **Xử lý token**
```javascript
// Lưu token vào memory
let accessToken = null;
// Hàm gọi API với token
export const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// Thêm interceptor để gắn token
api.interceptors.request.use(config => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// Xử lý lỗi 401
export function setupResponseInterceptor(logout) {
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const { accessToken: newToken } = await refreshToken();
accessToken = newToken;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (error) {
logout();
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
}
```
### Backend
1. **Cấu hình**
```yaml
auth:
access_token_expiry: 15m
refresh_token_expiry: 7d
jwt_secret: your-secret-key
refresh_secret: your-refresh-secret
```
2. **Sử dụng middleware**
```go
// Áp dụng auth middleware
router.Use(authMiddleware.Authenticate())
// Route yêu cầu đăng nhập
router.GET("/profile", userHandler.GetProfile)
// Route yêu cầu quyền admin
router.GET("/admin", authMiddleware.RequireRole("admin"), adminHandler.Dashboard)
```

View File

@ -1,116 +1,80 @@
# Đánh giá file `main.go`: Điểm bất thường và Đề xuất cải thiện 🚀 Cải Thiện Luồng Xác Thực Cho Project Starter-Kit
Một Starter-kit chất lượng cần có hệ thống xác thực được xây dựng trên các nguyên tắc bảo mật và thực hành tốt nhất. Dưới đây là những cải thiện quan trọng:
Dựa trên việc xem xét file `main.go` và các log liên quan, chúng ta đã xác định một số điểm trong thiết kế có thể dẫn đến hành vi khởi động không ổn định và log khó hiểu. 1. Bảo Mật Refresh Token (RT) Phía Client Ưu Tiên Hàng Đầu
Vấn đề cốt lõi: Lưu RT trong localStorage hoặc sessionStorage khiến chúng dễ bị tấn công XSS.
## I. Các điểm bất thường đã xác định Giải pháp cho Starter-kit:
1. **`HTTPService.Start()` trả về ngay lập tức và che giấu lỗi khởi động Server:** Sử dụng HttpOnly Cookies cho Refresh Token:
* **Mô tả:** Hàm `HTTPService.Start()` khởi chạy server HTTP (ví dụ: `s.server.Start()` của Gin) trong một goroutine riêng biệt và ngay lập tức trả về `nil` cho `lifecycleMgr`.
```go
// Trong HTTPService.Start()
go func() {
if err := s.server.Start(); err != nil {
logger.WithError(err).Error("HTTP server error")
}
}()
return nil // HTTPService.Start() trả về nil ngay
```
* **Hậu quả:**
* `lifecycleMgr` nhận được `nil` và cho rằng `HTTPService` đã khởi động thành công, ngay cả khi `s.server.Start()` trong goroutine gặp lỗi nghiêm trọng (ví dụ: cổng đã được sử dụng, lỗi cấu hình server) ngay sau đó.
* Lỗi thực sự của việc khởi động server HTTP không được truyền trở lại cho `lifecycleMgr`, khiến việc quản lý vòng đời và chẩn đoán lỗi trở nên khó khăn.
2. **Log "Application stopped" có thể xuất hiện không đúng thứ tự hoặc quá sớm:** Bắt buộc: Starter-kit NÊN mặc định hoặc hướng dẫn rõ ràng việc sử dụng cookie HttpOnly để lưu RT. Điều này ngăn JavaScript phía client truy cập RT.
* **Mô tả:** Dòng log `logger.Info("Application stopped")` nằm ở cuối hàm `main()` và chỉ được thực thi sau khi `lifecycleMgr.Wait()` kết thúc.
* **Hậu quả:**
* Do `HTTPService.Start()` trả về ngay và goroutine của server có thể lỗi và thoát một cách âm thầm, `lifecycleMgr.Wait()` có thể kết thúc sớm hơn dự kiến.
* Điều này dẫn đến "race condition" trong logging: log "Application stopped" có thể xuất hiện trước cả các log từ bên trong `s.server.Start()` (như "Starting HTTP server"), gây nhầm lẫn khi phân tích log.
3. **`lifecycleMgr` không nhận biết được trạng thái lỗi thực sự của HTTP Server:** Access Token (AT) có thể được lưu trong bộ nhớ JavaScript (an toàn hơn localStorage cho AT có đời sống ngắn) hoặc sessionStorage nếu cần thiết cho SPA.
* **Mô tả:** Vì `HTTPService.Start()` không phản ánh lỗi thực sự của việc khởi động server, `lifecycleMgr` không có thông tin chính xác về việc liệu `HTTPService` có đang hoạt động hay không.
* **Hậu quả:**
* Ứng dụng có thể "chết yểu" ở lần thử khởi động đầu tiên mà không có lỗi rõ ràng từ `lifecycleMgr`.
* Điều này có thể khiến các cơ chế bên ngoài (như Docker health checks hoặc trình giám sát tiến trình) phải khởi động lại container nhiều lần cho đến khi một lần khởi động ngẫu nhiên thành công (nếu lỗi mang tính tạm thời).
## II. Đề xuất cải thiện Thiết lập cờ Secure và SameSite cho Cookie:
Mục tiêu chính của các đề xuất này là đảm bảo rằng trạng thái khởi động thực sự của các service (đặc biệt là HTTP server) được phản ánh chính xác cho `lifecycleMgr`, giúp việc quản lý vòng đời và gỡ lỗi hiệu quả hơn. Secure: Đảm bảo cookie chỉ được gửi qua HTTPS.
1. **Cải thiện `HTTPService.Start()` để phản ánh trạng thái khởi động thực tế:** SameSite=Strict (hoặc SameSite=Lax): Giúp chống lại tấn công CSRF. Starter-kit NÊN có cấu hình này.
* **Phương án 1: Biến `HTTPService.Start()` thành hàm Blocking (Ưu tiên nếu phù hợp):** 2. Quản Lý Refresh Token Phía Server Đảm Bảo An Toàn
* Nếu `s.server.Start()` (ví dụ: `gin.Engine.Run()`) là một hàm blocking, hãy để `HTTPService.Start()` gọi trực tiếp nó mà không cần goroutine riêng. Thực hành tốt đã có: Refresh Token Rotation (xoay vòng RT khi sử dụng) là rất tốt.
* Nếu `s.server.Start()` trả về lỗi, `HTTPService.Start()` cũng phải trả về lỗi đó.
```go
// Đề xuất cho HTTPService.Start()
func (s *HTTPService) Start() error {
logger.Infof("Attempting to start %s on %s:%d...", s.Name(), s.cfg.Server.Host, s.cfg.Server.Port)
// Giả sử s.server.Start() là blocking và trả về lỗi nếu không khởi động được
if err := s.server.Start(); err != nil {
// Lỗi này sẽ được lifecycleMgr bắt và xử lý
return fmt.Errorf("failed to start %s: %w", s.Name(), err)
}
// Nếu s.server.Start() trả về mà không lỗi (thường chỉ khi shutdown),
// điều này có thể không bao giờ đạt tới nếu nó block mãi mãi.
// Hoặc nếu nó được thiết kế để trả về nil khi shutdown thành công từ bên ngoài.
return nil
}
```
* `lifecycleMgr` có thể sẽ cần tự quản lý việc chạy các hàm `Start()` blocking này trong các goroutine riêng của nó.
* **Phương án 2: Sử dụng Channel để giao tiếp trạng thái từ Goroutine:** Cải thiện cho Starter-kit:
* Nếu bắt buộc phải giữ `s.server.Start()` trong goroutine bên trong `HTTPService.Start()`, hãy sử dụng một channel để goroutine đó báo cáo lại trạng thái (khởi động thành công hoặc lỗi) cho `HTTPService.Start()`.
* `HTTPService.Start()` sẽ đợi tín hiệu này và trả về lỗi tương ứng nếu có.
```go
// Đề xuất cho HTTPService.Start() với channel
func (s *HTTPService) Start() error {
logger.Infof("Attempting to start %s on %s:%d...", s.Name(), s.cfg.Server.Host, s.cfg.Server.Port)
errChan := make(chan error, 1) // Buffered channel
go func() { Vô hiệu hóa RT cũ NGAY LẬP TỨC khi xoay vòng: Đảm bảo RT đã sử dụng không còn giá trị.
// Gửi lỗi (hoặc nil nếu thành công) vào channel
// Cần đảm bảo s.server.Start() hoặc logic bao quanh nó có thể báo hiệu thành công sớm
// Ví dụ, nếu server có một callback "onListenSuccess" hoặc tương tự
err := s.server.Start() // Giả sử hàm này block cho đến khi có lỗi hoặc shutdown
if err != nil && err != stdHttp.ErrServerClosed { // stdHttp từ "net/http"
logger.WithError(err).Errorf("Error running %s in goroutine", s.Name())
errChan <- fmt.Errorf("%s run error: %w", s.Name(), err)
return
}
logger.Infof("%s goroutine finished cleanly.", s.Name())
errChan <- nil // Báo hiệu kết thúc không lỗi (hoặc shutdown bình thường)
}()
// Đợi tín hiệu từ goroutine hoặc timeout Thu hồi RT khi Logout: Endpoint /api/v1/auth/logout PHẢI xóa hoặc đánh dấu RT là đã thu hồi trong cơ sở dữ liệu. Chỉ xóa ở client là không đủ.
// Cần một cách để goroutine báo "đã sẵn sàng" thay vì chỉ đợi lỗi/kết thúc
// Ví dụ, server có thể gửi nil vào errChan ngay khi lắng nghe thành công.
// Đoạn code dưới đây là một ví dụ đơn giản hóa việc chờ lỗi khởi động ban đầu.
// Một giải pháp tốt hơn là server có cơ chế báo "ready".
select {
case err := <-errChan:
if err != nil {
return fmt.Errorf("failed to start %s (from channel): %w", s.Name(), err)
}
logger.Infof("%s reported successful start or clean exit via channel.", s.Name())
return nil // Thành công hoặc goroutine đã kết thúc không lỗi nghiêm trọng ban đầu
case <-time.After(10 * time.Second): // Timeout nếu không tín hiệu sớm
// Giả định rằng nếu không có lỗi ngay, server đang chạy.
// Đây là một giả định cần được kiểm chứng cẩn thận với hành vi của s.server.Start()
logger.Warnf("Timeout waiting for %s to signal start/error, assuming it's running or will manage its own lifecycle.", s.Name())
return nil
}
}
```
**Lưu ý quan trọng cho Phương án 2:** Cần có một cơ chế rõ ràng để goroutine của server báo hiệu rằng nó đã **khởi động và lắng nghe thành công** (ví dụ: gửi `nil` vào `errChan` ngay khi `ListenAnServe` bắt đầu thành công), chứ không chỉ đợi đến khi có lỗi hoặc server dừng hẳn.
2. **Đảm bảo `lifecycleMgr` xử lý lỗi khởi động Service một cách nghiêm ngặt:** (Khuyến nghị cho Starter-kit nâng cao): Cân nhắc cơ chế phát hiện việc sử dụng RT đã bị đánh cắp (ví dụ: nếu một RT cũ được dùng lại sau khi đã xoay vòng, hãy thu hồi tất cả RT của user đó).
* Sau khi `HTTPService.Start()` (và các service khác) được sửa để trả về lỗi một cách chính xác, hãy đảm bảo rằng `lifecycleMgr.Start()` sẽ dừng toàn bộ quá trình nếu bất kỳ service nào không khởi động được.
```go
// Trong hàm main()
if err := lifecycleMgr.Start(); err != nil {
// Sử dụng Fatal để log lỗi và thoát ứng dụng ngay lập tức
logger.WithError(err).Fatalf("Critical failure: One or more services failed to start. Application shutting down.")
}
```
Điều này sẽ ngăn ứng dụng chạy trong trạng thái không ổn định và cung cấp thông tin lỗi rõ ràng ngay khi khởi động.
Bằng cách áp dụng những cải tiến này, bạn sẽ có một hệ thống khởi động mạnh mẽ hơn, dễ chẩn đoán lỗi hơn và các thông điệp log sẽ phản ánh chính xác hơn những gì đang thực sự xảy ra trong ứng dụng. 3. Tăng Cường Quy Trình Đăng Ký Nền Tảng Người Dùng
Cải thiện cho Starter-kit:
Chính Sách Mật Khẩu Tối Thiểu:
Yêu cầu độ dài mật khẩu tối thiểu (ví dụ: 8 hoặc 10 ký tự). Starter-kit NÊN có điều này.
(Tùy chọn): Khuyến khích hoặc yêu cầu kết hợp chữ hoa, chữ thường, số, ký tự đặc biệt.
Xác Thực Email (Khuyến Nghị Mạnh Mẽ):
Starter-kit NÊN bao gồm module hoặc hướng dẫn tích hợp quy trình gửi email xác thực để kích hoạt tài khoản. Điều này giúp đảm bảo email hợp lệ và là kênh liên lạc quan trọng.
4. Bảo Vệ Chống Tấn Công Đăng Nhập Lớp Phòng Thủ Cơ Bản
Cải thiện cho Starter-kit:
Rate Limiting cho Endpoint Đăng Nhập: Áp dụng giới hạn số lần thử đăng nhập thất bại (/api/v1/auth/login) dựa trên IP hoặc username/email.
Thông Báo Lỗi Chung Chung: Tránh các thông báo lỗi tiết lộ thông tin (ví dụ: "Username không tồn tại" hoặc "Sai mật khẩu"). Thay vào đó, sử dụng thông báo chung như "Tên đăng nhập hoặc mật khẩu không chính xác."
5. Thực Hành Tốt Nhất với JWT Cốt Lõi Của Xác Thực
Cải thiện cho Starter-kit:
Quản Lý Secret Key An Toàn:
Hướng dẫn lưu trữ JWT secret key trong biến môi trường (environment variables).
Tuyệt đối KHÔNG hardcode secret key trong mã nguồn.
Sử Dụng Thuật Toán Ký Mạnh:
Mặc định sử dụng thuật toán đối xứng mạnh như HS256.
Khuyến nghị và cung cấp tùy chọn cho thuật toán bất đối xứng như RS256 (yêu cầu quản lý cặp public/private key) cho các hệ thống phức tạp hơn.
Giữ Payload của Access Token Nhỏ Gọn:
Chỉ chứa thông tin cần thiết nhất (ví dụ: userId, roles).
Cân nhắc thêm iss (issuer) và aud (audience) để tăng cường xác minh token.
6. Xử Lý Lỗi và Ghi Log (Logging) An Toàn
Cải thiện cho Starter-kit:
Không Ghi Log Thông Tin Nhạy Cảm: Tuyệt đối KHÔNG ghi log Access Token, Refresh Token, hoặc mật khẩu dưới bất kỳ hình thức nào.
Ghi Log Sự Kiện An Ninh: Hướng dẫn hoặc cung cấp cơ chế ghi log các sự kiện quan trọng (đăng nhập thành công/thất bại, yêu cầu làm mới token, thay đổi mật khẩu) một cách an toàn, không kèm dữ liệu nhạy cảm, để phục vụ việc giám sát và điều tra.
Bằng cách tích hợp những cải tiến này, Starter-kit của bạn sẽ cung cấp một điểm khởi đầu vững chắc và an toàn hơn cho các nhà phát triển.

32
docs/session_20240524.md Normal file
View File

@ -0,0 +1,32 @@
# Tóm tắt phiên làm việc - 24/05/2025
## Các file đang mở
1. `docs/roadmap.md` - Đang xem mục tiêu phát triển
2. `configs/config.yaml` - File cấu hình ứng dụng
3. `docs/review.md` - Đang xem phần đánh giá code
## Các thay đổi chính trong phiên
### 1. Cập nhật Roadmap
- Đánh dấu hoàn thành các mục JWT Authentication
- Cập nhật chi tiết Giai đoạn 2 (Bảo mật và xác thực)
- Thêm timeline chi tiết cho từng tuần
### 2. Giải thích luồng xác thực
- Đã giải thích chi tiết về luồng JWT authentication
- Mô tả các endpoint chính và cách hoạt động
- Giải thích về bảo mật token và xử lý lỗi
### 3. Các lệnh đã sử dụng
- `/heyy` - Thảo luận về các bước tiếp theo
- `/yys` - Thử lưu trạng thái (không khả dụng)
## Công việc đang thực hiện
- Đang xem xét phần đánh giá code liên quan đến xử lý lỗi khởi động service
## Ghi chú
- Cần hoàn thiện phần Health Check API
- Cần triển khai API rate limiting và security headers
---
*Tự động tạo lúc: 2025-05-24 12:26*

2
go.mod
View File

@ -37,6 +37,7 @@ 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
@ -65,6 +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/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect

4
go.sum
View File

@ -169,6 +169,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -417,6 +419,8 @@ 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=

View File

@ -6,9 +6,14 @@ import (
"strings" "strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/joho/godotenv"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
const (
envFile = "./.env"
)
// ConfigLoader định nghĩa interface để load cấu hình // ConfigLoader định nghĩa interface để load cấu hình
type ConfigLoader interface { type ConfigLoader interface {
Load() (*Config, error) Load() (*Config, error)
@ -32,88 +37,93 @@ func NewConfigLoader() ConfigLoader {
} }
} }
// Load đọc cấu hình từ file và biến môi trường
func (l *ViperConfigLoader) Load() (*Config, error) { func (l *ViperConfigLoader) Load() (*Config, error) {
// Khởi tạo viper
v := viper.New() v := viper.New()
// Thiết lập tên config và loại
v.SetConfigName(l.configName) v.SetConfigName(l.configName)
v.SetConfigType(l.configType) v.SetConfigType(l.configType)
// Thêm các paths để tìm config // Thêm các đường dẫn tìm kiếm file cấu hình
for _, path := range l.configPaths { for _, path := range l.configPaths {
v.AddConfigPath(path) v.AddConfigPath(path)
} }
// Tự động đọc biến môi trường // 1. Đọc file cấu hình chính (bắt buộc)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w (searched in: %v)", err, l.configPaths)
}
// 2. Đọc từ file .env nếu tồn tại (tùy chọn)
if err := l.loadEnvFile(v); err != nil {
return nil, fmt.Errorf("error loading .env file: %w", err)
}
// 3. Cấu hình đọc biến môi trường (có thể ghi đè các giá trị từ file)
v.AutomaticEnv() v.AutomaticEnv()
v.SetEnvPrefix(l.envPrefix) v.SetEnvPrefix(l.envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// Thiết lập giá trị mặc định // 4. Đặt các giá trị mặc định tối thiểu
setDefaultValues(v) setDefaultValues(v)
// Đọc cấu hình
if err := v.ReadInConfig(); err != nil {
// Chỉ cảnh báo nếu không tìm thấy file, không gây lỗi
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("error reading config file: %v", err)
}
}
// Set default logger level
if os.Getenv("LOG_LEVEL") == "" {
if err := os.Setenv("LOG_LEVEL", "info"); err != nil {
return nil, fmt.Errorf("failed to set LOG_LEVEL: %v", err)
}
}
// Bind cấu hình vào struct // Bind cấu hình vào struct
var config Config var config Config
if err := v.Unmarshal(&config); err != nil { if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("unable to decode config into struct: %v", err) return nil, fmt.Errorf("unable to decode config into struct: %w", err)
} }
// Validate cấu hình // Validate cấu hình
if err := validateConfig(&config); err != nil { if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation error: %v", err) return nil, fmt.Errorf("config validation error: %w", err)
} }
return &config, nil return &config, nil
} }
// setDefaultValues thiết lập các giá trị mặc định cho config // loadEnvFile đọc và xử lý file .env
func setDefaultValues(v *viper.Viper) { func (l *ViperConfigLoader) loadEnvFile(v *viper.Viper) error {
// App defaults if _, err := os.Stat(envFile); os.IsNotExist(err) {
v.SetDefault("app.name", "GoStarter") return nil // Không có file .env cũng không phải lỗi
v.SetDefault("app.version", "0.1.0") }
v.SetDefault("app.environment", "development")
v.SetDefault("app.log_level", "info")
v.SetDefault("app.timezone", "UTC")
// Server defaults envMap, err := godotenv.Read(envFile)
v.SetDefault("server.host", "0.0.0.0") if err != nil {
v.SetDefault("server.port", 3000) return fmt.Errorf("error parsing .env file: %w", err)
v.SetDefault("server.read_timeout", 15) // seconds }
v.SetDefault("server.write_timeout", 15) // seconds
v.SetDefault("server.shutdown_timeout", 30) // seconds
v.SetDefault("server.trusted_proxies", []string{})
v.SetDefault("server.allow_origins", []string{"*"})
// Database defaults for key, val := range envMap {
v.SetDefault("database.driver", "postgres") if key == "" {
v.SetDefault("database.host", "localhost") continue
v.SetDefault("database.port", 5432) }
v.SetDefault("database.max_open_conns", 25)
v.SetDefault("database.max_idle_conns", 5) // Chuyển đổi key từ DB_PASSWORD thành database.password
v.SetDefault("database.conn_max_lifetime", 300) // seconds if parts := strings.SplitN(key, "_", 2); len(parts) == 2 {
v.SetDefault("database.ssl_mode", "disable") prefix := strings.ToLower(parts[0])
v.SetDefault("database.migration_path", "migrations") suffix := strings.ReplaceAll(parts[1], "_", ".")
v.Set(fmt.Sprintf("%s.%s", prefix, suffix), val)
}
// Lưu giá trị gốc cho tương thích ngược
v.Set(key, val)
os.Setenv(key, val)
}
return nil
} }
// validateConfig xác thực cấu hình // setDefaultValues thiết lập các giá trị mặc định tối thiểu cần thiết
// Lưu ý: Hầu hết các giá trị mặc định nên được định nghĩa trong file config.yaml
func setDefaultValues(v *viper.Viper) {
// Chỉ đặt các giá trị mặc định thực sự cần thiết ở đây
// Các giá trị khác sẽ được lấy từ file config.yaml bắt buộc
v.SetDefault("app.environment", "development")
v.SetDefault("log_level", "info")
}
// validateConfig xác thực cấu hình sử dụng thẻ validate
func validateConfig(config *Config) error { func validateConfig(config *Config) error {
validate := validator.New() validate := validator.New()
return validate.Struct(config) if err := validate.Struct(config); err != nil {
return fmt.Errorf("config validation failed: %w", err)
}
return nil
} }

View File

@ -34,6 +34,19 @@ type DatabaseConfig struct {
MigrationPath string `mapstructure:"migration_path" validate:"required"` MigrationPath string `mapstructure:"migration_path" validate:"required"`
} }
<<<<<<< Updated upstream
=======
// JWTConfig chứa cấu hình cho JWT
type JWTConfig struct {
Secret string `mapstructure:"secret" validate:"required,min=32"`
AccessTokenExpire int `mapstructure:"access_token_expire" validate:"required,min=1"` // in minutes
RefreshTokenExpire int `mapstructure:"refresh_token_expire" validate:"required,min=1"` // in days
Algorithm string `mapstructure:"algorithm" validate:"required,oneof=HS256 HS384 HS512 RS256"`
Issuer string `mapstructure:"issuer" validate:"required"`
Audience []string `mapstructure:"audience" validate:"required,min=1"`
}
>>>>>>> Stashed changes
// Config là struct tổng thể chứa tất cả các cấu hình // Config là struct tổng thể chứa tất cả các cấu hình
type Config struct { type Config struct {
App AppConfig `mapstructure:"app" validate:"required"` App AppConfig `mapstructure:"app" validate:"required"`

View File

@ -0,0 +1,110 @@
# 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

@ -0,0 +1,36 @@
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

@ -0,0 +1,84 @@
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.AllowedOrigins = []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) {
resp, err := http.Get(ts.URL + "/test")
assert.NoError(t, err)
defer resp.Body.Close()
// Kiểm tra CORS header
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"))
})
// 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)
resp.Body.Close()
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 resp.Body.Close()
// 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

@ -0,0 +1,141 @@
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

@ -0,0 +1,115 @@
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()
}
}

View File

@ -22,8 +22,29 @@ func SetupRouter(cfg *config.Config) *gin.Engine {
// Recovery middleware // Recovery middleware
router.Use(gin.Recovery()) router.Use(gin.Recovery())
<<<<<<< Updated upstream
// CORS middleware nếu cần // CORS middleware nếu cần
// router.Use(middleware.CORS()) // router.Use(middleware.CORS())
=======
// CORS middleware
router.Use(middleware.CORS(middleware.DefaultCORSConfig()))
// Khởi tạo repositories
userRepo := persistence.NewUserRepository(db)
roleRepo := persistence.NewRoleRepository(db)
// Khởi tạo services
authSvc := service.NewAuthService(
userRepo,
roleRepo,
cfg.JWT.Secret,
time.Duration(cfg.JWT.AccessTokenExpire)*time.Minute,
cfg.JWT.RefreshTokenExpire * 24, // Convert days to hours
)
// Khởi tạo middleware
authMiddleware := middleware.NewAuthMiddleware(authSvc)
>>>>>>> Stashed changes
// Khởi tạo các handlers // Khởi tạo các handlers
healthHandler := handler.NewHealthHandler(cfg) healthHandler := handler.NewHealthHandler(cfg)

View File

@ -1,29 +1,32 @@
# App Configuration # App Configuration
APP_APP_NAME="ULFlow Starter Kit" APP_NAME="ULFlow Starter Kit"
APP_APP_VERSION=0.1.0 APP_VERSION="0.1.0"
APP_APP_ENVIRONMENT=development APP_ENVIRONMENT="development"
APP_APP_TIMEZONE=Asia/Ho_Chi_Minh APP_TIMEZONE="Asia/Ho_Chi_Minh"
# Logger Configuration # Logger Configuration
APP_LOGGER_LEVEL=info # debug, info, warn, error LOG_LEVEL="info" # debug, info, warn, error
# Server Configuration # Server Configuration
APP_SERVER_HOST=0.0.0.0 SERVER_HOST="0.0.0.0"
APP_SERVER_PORT=3000 SERVER_PORT=3000
APP_SERVER_READ_TIMEOUT=15 SERVER_READ_TIMEOUT=15
APP_SERVER_WRITE_TIMEOUT=15 SERVER_WRITE_TIMEOUT=15
APP_SERVER_SHUTDOWN_TIMEOUT=30 SERVER_SHUTDOWN_TIMEOUT=30
APP_SERVER_ALLOW_ORIGINS=*
# Database Configuration # Database Configuration
APP_DATABASE_DRIVER=postgres DATABASE_DRIVER="postgres"
APP_DATABASE_HOST=localhost DATABASE_HOST="localhost"
APP_DATABASE_PORT=5432 DATABASE_PORT=5432
APP_DATABASE_USERNAME=postgres DATABASE_USERNAME="postgres"
APP_DATABASE_PASSWORD=postgres DATABASE_PASSWORD="postgres"
APP_DATABASE_DATABASE=ulflow DATABASE_NAME="ulflow"
APP_DATABASE_SSL_MODE=disable DATABASE_SSLMODE="disable"
APP_DATABASE_MAX_OPEN_CONNS=25
APP_DATABASE_MAX_IDLE_CONNS=5 # JWT Configuration
APP_DATABASE_CONN_MAX_LIFETIME=300 JWT_SECRET="your-32-byte-base64-encoded-secret-key-here"
APP_DATABASE_MIGRATION_PATH=migrations JWT_ACCESS_TOKEN_EXPIRE=15
JWT_REFRESH_TOKEN_EXPIRE=30
JWT_ALGORITHM="HS256"
JWT_ISSUER="ulflow-starter-kit"
JWT_AUDIENCE="ulflow-web"