feat: 封禁用户允许登录但禁止写操作
- Session 结构体新增 Status 字段,Create/Validate 同步状态 - AuthService.Login 移除封禁检查,允许封禁用户登录 - 新增 BannedWriteGuard 中间件,拦截 POST/PUT/DELETE 写操作 - 封禁用户自动签到跳过 - 所有需登录的 API 路由组应用 BannedWriteGuard - AdminService.UpdateUserStatus 保留 DestroyByUID 确保状态刷新
This commit is contained in:
@ -51,8 +51,6 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
switch err {
|
switch err {
|
||||||
case common.ErrInvalidCred:
|
case common.ErrInvalidCred:
|
||||||
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
||||||
case common.ErrUserBanned:
|
|
||||||
common.Error(c, http.StatusForbidden, "账号已被封禁")
|
|
||||||
case common.ErrUserLocked:
|
case common.ErrUserLocked:
|
||||||
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
common.Error(c, http.StatusUnauthorized, "邮箱或密码错误")
|
||||||
case common.ErrMaintenanceMode:
|
case common.ErrMaintenanceMode:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"metazone.cc/metalab/internal/common"
|
"metazone.cc/metalab/internal/common"
|
||||||
"metazone.cc/metalab/internal/config"
|
"metazone.cc/metalab/internal/config"
|
||||||
|
"metazone.cc/metalab/internal/model"
|
||||||
"metazone.cc/metalab/internal/session"
|
"metazone.cc/metalab/internal/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -86,10 +87,14 @@ func (am *AuthMiddleware) Optional() gin.HandlerFunc {
|
|||||||
// tryAutoCheckIn 尝试自动签到,并同步 session Exp 与 DB(静默失败,不影响主流程)
|
// tryAutoCheckIn 尝试自动签到,并同步 session Exp 与 DB(静默失败,不影响主流程)
|
||||||
// CheckIn 即使已签到也会返回 DB 中最新的 Exp,这里始终与 session 比较,
|
// CheckIn 即使已签到也会返回 DB 中最新的 Exp,这里始终与 session 比较,
|
||||||
// 确保 CompleteTask 等非签到路径的 Exp 变化也能同步到 session,避免 NAV 显示旧等级
|
// 确保 CompleteTask 等非签到路径的 Exp 变化也能同步到 session,避免 NAV 显示旧等级
|
||||||
|
// 被封禁用户跳过自动签到
|
||||||
func (am *AuthMiddleware) tryAutoCheckIn(c *gin.Context, s *session.Session) {
|
func (am *AuthMiddleware) tryAutoCheckIn(c *gin.Context, s *session.Session) {
|
||||||
if am.levelSvc == nil || s == nil {
|
if am.levelSvc == nil || s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.Status == model.StatusBanned {
|
||||||
|
return
|
||||||
|
}
|
||||||
_, newExp, _, _ := am.levelSvc.CheckIn(s.UserID)
|
_, newExp, _, _ := am.levelSvc.CheckIn(s.UserID)
|
||||||
// 只允许 Exp 只增不减:DB 异常返回 0 或主从延迟读到旧值时不会回退
|
// 只允许 Exp 只增不减:DB 异常返回 0 或主从延迟读到旧值时不会回退
|
||||||
if newExp > s.Exp {
|
if newExp > s.Exp {
|
||||||
@ -109,6 +114,31 @@ func injectSessionContext(c *gin.Context, s *session.Session) {
|
|||||||
c.Set("username", s.Username)
|
c.Set("username", s.Username)
|
||||||
c.Set("avatar", s.Avatar)
|
c.Set("avatar", s.Avatar)
|
||||||
c.Set("role", s.Role)
|
c.Set("role", s.Role)
|
||||||
|
c.Set("status", s.Status)
|
||||||
c.Set("exp", s.Exp)
|
c.Set("exp", s.Exp)
|
||||||
c.Set("sid", s.ID)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
adminAPI := r.Group("/api/admin")
|
adminAPI := r.Group("/api/admin")
|
||||||
adminAPI.Use(d.authMdw.Required())
|
adminAPI.Use(d.authMdw.Required())
|
||||||
adminAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
adminAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
||||||
|
adminAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
adminAPI.Use(middleware.CSRF(cfg))
|
adminAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
adminAPI.GET("/users", d.adminCtrl.ListUsers)
|
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 := r.Group("/api/admin/site-settings")
|
||||||
siteSettingsAPI.Use(d.authMdw.Required())
|
siteSettingsAPI.Use(d.authMdw.Required())
|
||||||
siteSettingsAPI.Use(middleware.RequireMinRole(model.RoleOwner))
|
siteSettingsAPI.Use(middleware.RequireMinRole(model.RoleOwner))
|
||||||
|
siteSettingsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
siteSettingsAPI.Use(middleware.CSRF(cfg))
|
siteSettingsAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
siteSettingsAPI.GET("", d.siteSettingCtrl.GetSettings)
|
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 := r.Group("/api/admin/posts")
|
||||||
adminPostAPI.Use(d.authMdw.Required())
|
adminPostAPI.Use(d.authMdw.Required())
|
||||||
adminPostAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
adminPostAPI.Use(middleware.RequireMinRole(model.RoleAdmin))
|
||||||
|
adminPostAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
adminPostAPI.Use(middleware.CSRF(cfg))
|
adminPostAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
adminPostAPI.POST("/:id/approve", d.adminPostCtrl.Approve)
|
adminPostAPI.POST("/:id/approve", d.adminPostCtrl.Approve)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
// --- 设置页 API(需登录) ---
|
// --- 设置页 API(需登录) ---
|
||||||
settingsAPI := r.Group("/api/settings")
|
settingsAPI := r.Group("/api/settings")
|
||||||
settingsAPI.Use(d.authMdw.Required())
|
settingsAPI.Use(d.authMdw.Required())
|
||||||
|
settingsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
settingsAPI.Use(middleware.CSRF(cfg))
|
settingsAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
settingsAPI.PUT("/profile", d.settingsCtrl.UpdateProfile)
|
settingsAPI.PUT("/profile", d.settingsCtrl.UpdateProfile)
|
||||||
@ -29,6 +30,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
// --- 消息中心 API(需登录) ---
|
// --- 消息中心 API(需登录) ---
|
||||||
messagesAPI := r.Group("/api/messages")
|
messagesAPI := r.Group("/api/messages")
|
||||||
messagesAPI.Use(d.authMdw.Required())
|
messagesAPI.Use(d.authMdw.Required())
|
||||||
|
messagesAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
messagesAPI.Use(middleware.CSRF(cfg))
|
messagesAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
messagesAPI.GET("/unread", d.messageCtrl.UnreadCount)
|
messagesAPI.GET("/unread", d.messageCtrl.UnreadCount)
|
||||||
@ -39,6 +41,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
// --- 积分/等级 API(需登录) ---
|
// --- 积分/等级 API(需登录) ---
|
||||||
levelAPI := r.Group("/api/level")
|
levelAPI := r.Group("/api/level")
|
||||||
levelAPI.Use(d.authMdw.Required())
|
levelAPI.Use(d.authMdw.Required())
|
||||||
|
levelAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
levelAPI.Use(middleware.CSRF(cfg))
|
levelAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
levelAPI.POST("/checkin", d.levelCtrl.CheckIn)
|
levelAPI.POST("/checkin", d.levelCtrl.CheckIn)
|
||||||
@ -64,6 +67,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
// --- 帖子 API(需登录) ---
|
// --- 帖子 API(需登录) ---
|
||||||
postsAPI := r.Group("/api/posts")
|
postsAPI := r.Group("/api/posts")
|
||||||
postsAPI.Use(d.authMdw.Required())
|
postsAPI.Use(d.authMdw.Required())
|
||||||
|
postsAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
postsAPI.Use(middleware.CSRF(cfg))
|
postsAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
postsAPI.POST("/:id/submit", d.postCtrl.Submit)
|
postsAPI.POST("/:id/submit", d.postCtrl.Submit)
|
||||||
@ -73,6 +77,7 @@ func setupAPIRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
|
|||||||
// --- 创作中心 API(需登录) ---
|
// --- 创作中心 API(需登录) ---
|
||||||
studioAPI := r.Group("/api/studio")
|
studioAPI := r.Group("/api/studio")
|
||||||
studioAPI.Use(d.authMdw.Required())
|
studioAPI.Use(d.authMdw.Required())
|
||||||
|
studioAPI.Use(d.authMdw.BannedWriteGuard())
|
||||||
studioAPI.Use(middleware.CSRF(cfg))
|
studioAPI.Use(middleware.CSRF(cfg))
|
||||||
{
|
{
|
||||||
studioAPI.GET("/overview", d.studioCtrl.OverviewAPI)
|
studioAPI.GET("/overview", d.studioCtrl.OverviewAPI)
|
||||||
|
|||||||
@ -148,12 +148,6 @@ func (s *AuthService) Login(req model.LoginRequest, loginIP string) (*model.User
|
|||||||
return nil, common.ErrUserLocked
|
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 {
|
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil {
|
||||||
return nil, common.ErrInvalidCred
|
return nil, common.ErrInvalidCred
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
|
|||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Avatar: user.Avatar,
|
Avatar: user.Avatar,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
|
Status: user.Status,
|
||||||
Exp: user.Exp,
|
Exp: user.Exp,
|
||||||
RememberMe: rememberMe,
|
RememberMe: rememberMe,
|
||||||
IP: ip,
|
IP: ip,
|
||||||
@ -54,7 +55,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate 验证会话:读取 → 检查过期 → 检查用户状态 → 续期 → 返回
|
// Validate 验证会话:读取 → 检查过期 → 检查用户状态 → 续期 → 返回
|
||||||
// 返回 nil 表示会话无效(不存在/过期/用户被封禁)
|
// 若用户不存在则返回 nil;被封禁/锁定用户仍返回 session(由上层中间件决定可访问范围)
|
||||||
func (m *Manager) Validate(sid string) (*Session, error) {
|
func (m *Manager) Validate(sid string) (*Session, error) {
|
||||||
if sid == "" {
|
if sid == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -65,7 +66,7 @@ func (m *Manager) Validate(sid string) (*Session, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否仍然有效(封禁/注销后拒绝已有会话)
|
// 检查用户是否仍然存在
|
||||||
user, err := m.userRepo.FindByIDForAuth(s.UserID)
|
user, err := m.userRepo.FindByIDForAuth(s.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[SessionManager] Validate: user not found uid=%d err=%v", s.UserID, err)
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封禁用户直接拒绝
|
// 同步 DB 最新字段(头像/用户名/角色/状态/经验值等可能已变更)
|
||||||
if user.Status == model.StatusBanned {
|
|
||||||
log.Printf("[SessionManager] Validate: user banned uid=%d", s.UserID)
|
|
||||||
m.store.Delete(sid)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同步 DB 最新字段(头像/用户名/角色/经验值等可能已变更)
|
|
||||||
s.Avatar = user.Avatar
|
s.Avatar = user.Avatar
|
||||||
s.Username = user.Username
|
s.Username = user.Username
|
||||||
s.Role = user.Role
|
s.Role = user.Role
|
||||||
|
s.Status = user.Status
|
||||||
s.Exp = user.Exp
|
s.Exp = user.Exp
|
||||||
|
|
||||||
// 滑动窗口续期
|
// 滑动窗口续期
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type Session struct {
|
|||||||
Username string // 用户名
|
Username string // 用户名
|
||||||
Avatar string // 头像 URL
|
Avatar string // 头像 URL
|
||||||
Role string // 角色
|
Role string // 角色
|
||||||
|
Status string // 账号状态(active/banned/deleted/locked)
|
||||||
Exp int // 经验值(用于前端实时展示等级)
|
Exp int // 经验值(用于前端实时展示等级)
|
||||||
RememberMe bool // 是否持久化(决定 cookie maxAge)
|
RememberMe bool // 是否持久化(决定 cookie maxAge)
|
||||||
IP string // 登录 IP
|
IP string // 登录 IP
|
||||||
|
|||||||
Reference in New Issue
Block a user