From bbcd99261431efb5471cbcfff0353167e78f8056 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Sun, 31 May 2026 21:20:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=81=E7=A6=81=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E7=99=BB=E5=BD=95=E4=BD=86=E7=A6=81=E6=AD=A2?= =?UTF-8?q?=E5=86=99=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Session 结构体新增 Status 字段,Create/Validate 同步状态 - AuthService.Login 移除封禁检查,允许封禁用户登录 - 新增 BannedWriteGuard 中间件,拦截 POST/PUT/DELETE 写操作 - 封禁用户自动签到跳过 - 所有需登录的 API 路由组应用 BannedWriteGuard - AdminService.UpdateUserStatus 保留 DestroyByUID 确保状态刷新 --- internal/controller/auth_api_login.go | 2 -- internal/middleware/auth.go | 30 +++++++++++++++++++++++++++ internal/router/admin.go | 3 +++ internal/router/api.go | 5 +++++ internal/service/auth_service.go | 6 ------ internal/session/manager.go | 15 +++++--------- internal/session/session.go | 1 + 7 files changed, 44 insertions(+), 18 deletions(-) diff --git a/internal/controller/auth_api_login.go b/internal/controller/auth_api_login.go index 33a1af5..5c0f4b3 100644 --- a/internal/controller/auth_api_login.go +++ b/internal/controller/auth_api_login.go @@ -51,8 +51,6 @@ func (ac *AuthController) Login(c *gin.Context) { switch err { case common.ErrInvalidCred: common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") - case common.ErrUserBanned: - common.Error(c, http.StatusForbidden, "账号已被封禁") case common.ErrUserLocked: common.Error(c, http.StatusUnauthorized, "邮箱或密码错误") case common.ErrMaintenanceMode: diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 9fe17f0..e07a403 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -5,6 +5,7 @@ import ( "metazone.cc/metalab/internal/common" "metazone.cc/metalab/internal/config" + "metazone.cc/metalab/internal/model" "metazone.cc/metalab/internal/session" "github.com/gin-gonic/gin" @@ -86,10 +87,14 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc { // tryAutoCheckIn 尝试自动签到,并同步 session Exp 与 DB(静默失败,不影响主流程) // CheckIn 即使已签到也会返回 DB 中最新的 Exp,这里始终与 session 比较, // 确保 CompleteTask 等非签到路径的 Exp 变化也能同步到 session,避免 NAV 显示旧等级 +// 被封禁用户跳过自动签到 func (am *AuthMiddleware) tryAutoCheckIn(c *gin.Context, s *session.Session) { if am.levelSvc == nil || s == nil { return } + if s.Status == model.StatusBanned { + return + } _, newExp, _, _ := am.levelSvc.CheckIn(s.UserID) // 只允许 Exp 只增不减:DB 异常返回 0 或主从延迟读到旧值时不会回退 if newExp > s.Exp { @@ -109,6 +114,31 @@ func injectSessionContext(c *gin.Context, s *session.Session) { c.Set("username", s.Username) c.Set("avatar", s.Avatar) c.Set("role", s.Role) + c.Set("status", s.Status) c.Set("exp", s.Exp) c.Set("sid", s.ID) } + +// BannedWriteGuard 封禁用户写操作守卫 +// 仅拦截 POST/PUT/DELETE/PATCH 请求,GET/HEAD/OPTIONS 放行 +// 需在 Required() 之后调用,确保上下文中已有用户 status +func (am *AuthMiddleware) BannedWriteGuard() gin.HandlerFunc { + return func(c *gin.Context) { + // 只检查写操作 + switch c.Request.Method { + case "GET", "HEAD", "OPTIONS": + c.Next() + return + } + + status, _ := c.Get("status") + if status == model.StatusBanned { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "账号已被封禁,无法执行此操作", + }) + return + } + c.Next() + } +} diff --git a/internal/router/admin.go b/internal/router/admin.go index 6203d5e..ada6258 100644 --- a/internal/router/admin.go +++ b/internal/router/admin.go @@ -34,6 +34,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { adminAPI := r.Group("/api/admin") adminAPI.Use(d.authMdw.Required()) adminAPI.Use(middleware.RequireMinRole(model.RoleAdmin)) + adminAPI.Use(d.authMdw.BannedWriteGuard()) adminAPI.Use(middleware.CSRF(cfg)) { adminAPI.GET("/users", d.adminCtrl.ListUsers) @@ -48,6 +49,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { siteSettingsAPI := r.Group("/api/admin/site-settings") siteSettingsAPI.Use(d.authMdw.Required()) siteSettingsAPI.Use(middleware.RequireMinRole(model.RoleOwner)) + siteSettingsAPI.Use(d.authMdw.BannedWriteGuard()) siteSettingsAPI.Use(middleware.CSRF(cfg)) { siteSettingsAPI.GET("", d.siteSettingCtrl.GetSettings) @@ -60,6 +62,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { adminPostAPI := r.Group("/api/admin/posts") adminPostAPI.Use(d.authMdw.Required()) adminPostAPI.Use(middleware.RequireMinRole(model.RoleAdmin)) + adminPostAPI.Use(d.authMdw.BannedWriteGuard()) adminPostAPI.Use(middleware.CSRF(cfg)) { adminPostAPI.POST("/:id/approve", d.adminPostCtrl.Approve) diff --git a/internal/router/api.go b/internal/router/api.go index edd7cc3..64211e1 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -12,6 +12,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { // --- 设置页 API(需登录) --- settingsAPI := r.Group("/api/settings") settingsAPI.Use(d.authMdw.Required()) + settingsAPI.Use(d.authMdw.BannedWriteGuard()) settingsAPI.Use(middleware.CSRF(cfg)) { settingsAPI.PUT("/profile", d.settingsCtrl.UpdateProfile) @@ -29,6 +30,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { // --- 消息中心 API(需登录) --- messagesAPI := r.Group("/api/messages") messagesAPI.Use(d.authMdw.Required()) + messagesAPI.Use(d.authMdw.BannedWriteGuard()) messagesAPI.Use(middleware.CSRF(cfg)) { messagesAPI.GET("/unread", d.messageCtrl.UnreadCount) @@ -39,6 +41,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { // --- 积分/等级 API(需登录) --- levelAPI := r.Group("/api/level") levelAPI.Use(d.authMdw.Required()) + levelAPI.Use(d.authMdw.BannedWriteGuard()) levelAPI.Use(middleware.CSRF(cfg)) { levelAPI.POST("/checkin", d.levelCtrl.CheckIn) @@ -64,6 +67,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { // --- 帖子 API(需登录) --- postsAPI := r.Group("/api/posts") postsAPI.Use(d.authMdw.Required()) + postsAPI.Use(d.authMdw.BannedWriteGuard()) postsAPI.Use(middleware.CSRF(cfg)) { postsAPI.POST("/:id/submit", d.postCtrl.Submit) @@ -73,6 +77,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { // --- 创作中心 API(需登录) --- studioAPI := r.Group("/api/studio") studioAPI.Use(d.authMdw.Required()) + studioAPI.Use(d.authMdw.BannedWriteGuard()) studioAPI.Use(middleware.CSRF(cfg)) { studioAPI.GET("/overview", d.studioCtrl.OverviewAPI) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index b3207ae..bdc436c 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -148,12 +148,6 @@ func (s *AuthService) Login(req model.LoginRequest, loginIP string) (*model.User return nil, common.ErrUserLocked } - // 封禁 - if user.Status == model.StatusBanned { - _ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password)) - return nil, common.ErrUserBanned - } - if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil { return nil, common.ErrInvalidCred } diff --git a/internal/session/manager.go b/internal/session/manager.go index 6acbd13..d23713c 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -39,6 +39,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string Username: user.Username, Avatar: user.Avatar, Role: user.Role, + Status: user.Status, Exp: user.Exp, RememberMe: rememberMe, IP: ip, @@ -54,7 +55,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string } // Validate 验证会话:读取 → 检查过期 → 检查用户状态 → 续期 → 返回 -// 返回 nil 表示会话无效(不存在/过期/用户被封禁) +// 若用户不存在则返回 nil;被封禁/锁定用户仍返回 session(由上层中间件决定可访问范围) func (m *Manager) Validate(sid string) (*Session, error) { if sid == "" { return nil, nil @@ -65,7 +66,7 @@ func (m *Manager) Validate(sid string) (*Session, error) { return nil, nil } - // 检查用户是否仍然有效(封禁/注销后拒绝已有会话) + // 检查用户是否仍然存在 user, err := m.userRepo.FindByIDForAuth(s.UserID) if err != nil { log.Printf("[SessionManager] Validate: user not found uid=%d err=%v", s.UserID, err) @@ -73,17 +74,11 @@ func (m *Manager) Validate(sid string) (*Session, error) { return nil, nil } - // 封禁用户直接拒绝 - if user.Status == model.StatusBanned { - log.Printf("[SessionManager] Validate: user banned uid=%d", s.UserID) - m.store.Delete(sid) - return nil, nil - } - - // 同步 DB 最新字段(头像/用户名/角色/经验值等可能已变更) + // 同步 DB 最新字段(头像/用户名/角色/状态/经验值等可能已变更) s.Avatar = user.Avatar s.Username = user.Username s.Role = user.Role + s.Status = user.Status s.Exp = user.Exp // 滑动窗口续期 diff --git a/internal/session/session.go b/internal/session/session.go index a674698..b22923f 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -14,6 +14,7 @@ type Session struct { Username string // 用户名 Avatar string // 头像 URL Role string // 角色 + Status string // 账号状态(active/banned/deleted/locked) Exp int // 经验值(用于前端实时展示等级) RememberMe bool // 是否持久化(决定 cookie maxAge) IP string // 登录 IP