Files
user-system/cmd/server/main.go
long-agent 3ae11237ab fix: P1/P2 优化 - OAuth验证 + API响应 + 缓存击穿 + Webhook关闭
P1 - OAuth auth_url origin 验证:
- 添加 validateOAuthUrl() 函数验证 OAuth URL origin
- 仅允许同源或可信 OAuth 提供商
- LoginPage 和 ProfileSecurityPage 调用前验证

P2 - API 响应运行时类型验证:
- 添加 isApiResponse() 运行时验证函数
- parseJsonResponse 验证响应结构完整性

P2 - 缓存击穿防护 (singleflight):
- AuthMiddleware.isJTIBlacklisted 使用 singleflight.Group
- 防止 L1 miss 时并发请求同时打 L2

P2 - Webhook 服务优雅关闭:
- WebhookService 添加 Shutdown() 方法
- 服务器关闭时等待 worker 完成
- main.go 集成 shutdown 调用
2026-04-03 21:50:51 +08:00

245 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/api/handler"
"github.com/user-management-system/internal/api/middleware"
"github.com/user-management-system/internal/api/router"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/config"
"github.com/user-management-system/internal/database"
"github.com/user-management-system/internal/repository"
"github.com/user-management-system/internal/security"
"github.com/user-management-system/internal/service"
)
func main() {
// 加载配置
cfg, err := config.Load()
if err != nil {
log.Fatalf("load config failed: %v", err)
}
// 设置 Gin 模式
gin.SetMode(resolveGinMode(cfg.Server.Mode))
// 初始化数据库
db, err := database.NewDB(cfg)
if err != nil {
log.Fatalf("connect database failed: %v", err)
}
// 执行数据库迁移
if err := db.AutoMigrate(cfg); err != nil {
log.Fatalf("auto migrate failed: %v", err)
}
// 初始化 JWT 管理器
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: cfg.JWT.Secret,
AccessTokenExpire: time.Duration(cfg.JWT.AccessTokenExpireMinutes) * time.Minute,
RefreshTokenExpire: time.Duration(cfg.JWT.RefreshTokenExpireDays) * 24 * time.Hour,
})
if err != nil {
log.Fatalf("create jwt manager failed: %v", err)
}
// 初始化缓存
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCacheWithConfig(cache.RedisCacheConfig{
Addr: fmt.Sprintf("%s:%d", cfg.Redis.Host, cfg.Redis.Port),
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
defer l2Cache.Close()
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
// 初始化 Repository
userRepo := repository.NewUserRepository(db.DB)
roleRepo := repository.NewRoleRepository(db.DB)
permissionRepo := repository.NewPermissionRepository(db.DB)
userRoleRepo := repository.NewUserRoleRepository(db.DB)
rolePermissionRepo := repository.NewRolePermissionRepository(db.DB)
deviceRepo := repository.NewDeviceRepository(db.DB)
loginLogRepo := repository.NewLoginLogRepository(db.DB)
operationLogRepo := repository.NewOperationLogRepository(db.DB)
customFieldRepo := repository.NewCustomFieldRepository(db.DB)
userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db.DB)
themeRepo := repository.NewThemeConfigRepository(db.DB)
socialRepo, err := repository.NewSocialAccountRepository(db.DB)
if err != nil {
log.Fatalf("initialize social account repository failed: %v", err)
}
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
// 初始化 Service
deviceService := service.NewDeviceService(deviceRepo, userRepo)
authService := service.NewAuthService(
userRepo,
socialRepo,
jwtManager,
cacheManager,
8, // passwordMinLength
5, // maxLoginAttempts
15*time.Minute, // loginLockDuration
)
authService.SetRoleRepositories(userRoleRepo, roleRepo)
authService.SetLoginLogRepository(loginLogRepo)
authService.SetDeviceService(deviceService)
// IP 过滤中间件
var ipFilterMiddleware *middleware.IPFilterMiddleware
ipFilter := security.NewIPFilter()
if ipFilter != nil {
ipFilterMiddleware = middleware.NewIPFilterMiddleware(ipFilter, middleware.IPFilterConfig{
TrustProxy: cfg.CORS.AllowCredentials,
})
}
// 初始化异常检测器并注入
anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter)
authService.SetAnomalyDetector(anomalyDetector)
log.Println("anomaly detector initialized")
userService := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo)
roleService := service.NewRoleService(roleRepo, rolePermissionRepo)
permissionService := service.NewPermissionService(permissionRepo)
loginLogService := service.NewLoginLogService(loginLogRepo)
operationLogService := service.NewOperationLogService(operationLogRepo)
captchaService := service.NewCaptchaService(cacheManager)
totpService := service.NewTOTPService(userRepo)
passwordResetConfig := service.DefaultPasswordResetConfig()
passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig).
WithPasswordHistoryRepo(passwordHistoryRepo)
webhookService := service.NewWebhookService(db.DB, service.WebhookServiceConfig{
Enabled: false,
})
exportService := service.NewExportService(userRepo, roleRepo)
statsService := service.NewStatsService(userRepo, loginLogRepo)
customFieldService := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo)
themeService := service.NewThemeService(themeRepo)
// 设置 CORS 配置
middleware.SetCORSConfig(cfg.CORS)
// 初始化中间件
rateLimitMiddleware := middleware.NewRateLimitMiddleware(cfg.RateLimit)
authMiddleware := middleware.NewAuthMiddleware(
jwtManager,
userRepo,
userRoleRepo,
roleRepo,
rolePermissionRepo,
permissionRepo,
l1Cache,
)
authMiddleware.SetCacheManager(cacheManager)
opLogMiddleware := middleware.NewOperationLogMiddleware(operationLogRepo)
// 初始化 Handler
authHandler := handler.NewAuthHandler(authService)
userHandler := handler.NewUserHandler(userService)
roleHandler := handler.NewRoleHandler(roleService)
permissionHandler := handler.NewPermissionHandler(permissionService)
deviceHandler := handler.NewDeviceHandler(deviceService)
logHandler := handler.NewLogHandler(loginLogService, operationLogService)
captchaHandler := handler.NewCaptchaHandler(captchaService)
totpHandler := handler.NewTOTPHandler(authService, totpService)
webhookHandler := handler.NewWebhookHandler(webhookService)
exportHandler := handler.NewExportHandler(exportService)
statsHandler := handler.NewStatsHandler(statsService)
passwordResetHandler := handler.NewPasswordResetHandler(passwordResetService)
smsHandler := handler.NewSMSHandler()
avatarHandler := handler.NewAvatarHandler()
customFieldHandler := handler.NewCustomFieldHandler(customFieldService)
themeHandler := handler.NewThemeHandler(themeService)
// 初始化 SSO 管理器
ssoManager := auth.NewSSOManager()
ssoClientsStore := auth.NewDefaultSSOClientsStore()
ssoHandler := handler.NewSSOHandler(ssoManager, ssoClientsStore)
// SSO 会话清理 context随服务器关闭而取消
ssoCtx, ssoCancel := context.WithCancel(context.Background())
defer ssoCancel()
ssoManager.StartCleanup(ssoCtx)
// 设置路由
r := router.NewRouter(
authHandler, userHandler, roleHandler, permissionHandler, deviceHandler,
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
passwordResetHandler, captchaHandler, totpHandler, webhookHandler,
ipFilterMiddleware, exportHandler, statsHandler, smsHandler, customFieldHandler, themeHandler, ssoHandler, avatarHandler,
)
engine := r.Setup()
// 健康检查
engine.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// 启动服务器
addr := fmt.Sprintf(":%d", cfg.Server.Port)
srv := &http.Server{
Addr: addr,
Handler: engine,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
go func() {
log.Printf("server listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen failed: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
// 关闭 Webhook 服务,等待投递任务完成
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := webhookService.Shutdown(shutdownCtx); err != nil {
log.Printf("webhook service shutdown: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("server exited")
}
func resolveGinMode(mode string) string {
switch mode {
case "debug":
return gin.DebugMode
case "test":
return gin.TestMode
default:
return gin.ReleaseMode
}
}