diff --git a/internal/middleware/rate.go b/internal/middleware/rate.go new file mode 100644 index 0000000..255dfb2 --- /dev/null +++ b/internal/middleware/rate.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// SensitiveRateLimit 敏感操作(改密/注销/签到)IP 维度限流中间件。 +// 1 分钟最多 5 次请求,超限后封禁 5 分钟。 +func SensitiveRateLimit(rl *RateLimiter) gin.HandlerFunc { + return func(c *gin.Context) { + result := rl.AllowSensitive(c.ClientIP()) + if result.Blocked { + c.JSON(http.StatusTooManyRequests, gin.H{ + "success": false, + "error": result.Message, + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go index 941a9c3..cc7e7ab 100644 --- a/internal/middleware/ratelimit.go +++ b/internal/middleware/ratelimit.go @@ -32,3 +32,8 @@ func (rl *RateLimiter) AllowAccount(email string) RateLimitResult { func (rl *RateLimiter) AllowIP(ip string) RateLimitResult { return rl.try(rl.ipFailures, ip, ipWindow, ipBlockDur, ipMaxFails, false) } + +// AllowSensitive 敏感操作限流(改密/注销/签到),基于 IP,1 分钟最多 5 次,超限封禁 5 分钟。 +func (rl *RateLimiter) AllowSensitive(ip string) RateLimitResult { + return rl.try(rl.ipFailures, "sensitive:"+ip, sensitiveWindow, sensitiveBlockDur, sensitiveMaxRequests, false) +} diff --git a/internal/middleware/ratelimit_core.go b/internal/middleware/ratelimit_core.go index 94b92d7..d0adf0d 100644 --- a/internal/middleware/ratelimit_core.go +++ b/internal/middleware/ratelimit_core.go @@ -21,6 +21,11 @@ const ( ipBlockDur = 1 * time.Minute cleanupInterval = 2 * time.Minute maxEntries = 10000 + + // 敏感操作限流:改密/注销/签到 + sensitiveWindow = 1 * time.Minute + sensitiveMaxRequests = 5 + sensitiveBlockDur = 5 * time.Minute ) // windowState 单个维度的限流状态 diff --git a/internal/router/api.go b/internal/router/api.go index 291c307..c07a590 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -17,8 +17,8 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { { settingsAPI.PUT("/profile", d.settingsCtrl.UpdateProfile) settingsAPI.POST("/avatar", d.settingsCtrl.UploadAvatar) - settingsAPI.PUT("/password", d.settingsCtrl.ChangePassword) - settingsAPI.POST("/delete-account", d.settingsCtrl.DeleteAccount) + settingsAPI.PUT("/password", middleware.SensitiveRateLimit(d.rateLimiter), d.settingsCtrl.ChangePassword) + settingsAPI.POST("/delete-account", middleware.SensitiveRateLimit(d.rateLimiter), d.settingsCtrl.DeleteAccount) settingsAPI.GET("/audit-status", d.settingsCtrl.AuditStatus) // 登录管理 settingsAPI.GET("/sessions", d.settingsCtrl.ListSessions) @@ -47,7 +47,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { levelAPI.Use(d.authMdw.BannedWriteGuard()) levelAPI.Use(middleware.CSRF(cfg)) { - levelAPI.POST("/checkin", d.levelCtrl.CheckIn) + levelAPI.POST("/checkin", middleware.SensitiveRateLimit(d.rateLimiter), d.levelCtrl.CheckIn) levelAPI.GET("/info", d.levelCtrl.GetLevel) } diff --git a/internal/router/deps_core.go b/internal/router/deps_core.go index 945dba5..3ece49e 100644 --- a/internal/router/deps_core.go +++ b/internal/router/deps_core.go @@ -20,6 +20,7 @@ import ( type dependencies struct { authCtrl *controller.AuthController authMdw *middleware.AuthMiddleware + rateLimiter *middleware.RateLimiter maintenanceMdw *middleware.MaintenanceMiddleware settingsCtrl *controller.SettingsController messageCtrl *controller.MessageController diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index 110ed70..519ac8c 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -16,7 +16,7 @@ import ( ) func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) *dependencies { - userRepo, sessionMgr, authService, _, authCtrl, avatarSvc, authMdw := buildDepsCore(db, cfg, siteSettings, redisClient) + userRepo, sessionMgr, authService, rateLimiter, authCtrl, avatarSvc, authMdw := buildDepsCore(db, cfg, siteSettings, redisClient) auditRepo := repository.NewAuditRepo(db) notificationRepo := repository.NewNotificationRepo(db) @@ -107,7 +107,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting postService.WithHomePageInvalidator(func() { homeCache.Invalidate() }) return &dependencies{ - authCtrl: authCtrl, authMdw: authMdw, maintenanceMdw: maintenanceMdw, + authCtrl: authCtrl, authMdw: authMdw, rateLimiter: rateLimiter, maintenanceMdw: maintenanceMdw, settingsCtrl: settingsCtrl, messageCtrl: messageController, postCtrl: postCtrl, postService: postService, spaceCtrl: spaceCtrl, studioCtrl: studioCtrl,