package middleware import ( "errors" "net/http" "strings" "github.com/freefire/jiang13-bbs/model" "github.com/freefire/jiang13-bbs/service" "github.com/gin-gonic/gin" ) // AccountBannedKey 写入 gin.Context 的标记:当前凭据所属账号已被封禁。 // parseToken 失败原因对 handler 不可见,通过 context 显式传递, // 以便 /me 等 OptionalAuth 接口也能告知前端"被封禁"而非"未登录"。 const AccountBannedKey = "account_banned" // 封禁响应体:code 供前端机器识别,error 供直接展示 func bannedJSON(c *gin.Context) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "账号已被封禁", "code": "account_banned", }) } // AuthMiddleware 认证中间件 type AuthMiddleware struct { auth *service.AuthService } func NewAuthMiddleware(auth *service.AuthService) *AuthMiddleware { return &AuthMiddleware{auth: auth} } // OptionalAuth 可选登录:解析 token,失败不阻断 func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc { return func(c *gin.Context) { user, ok := m.parseToken(c) if ok { c.Set("user", user) } c.Next() } } // RequireAuth 必须登录 func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { user, ok := m.parseToken(c) if !ok { if c.GetBool(AccountBannedKey) { bannedJSON(c) return } c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } c.Set("user", user) c.Next() } } // RequireAdmin 必须管理员(保留兼容;等价于"管理员及以上",不含纯板块管理员) func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc { return func(c *gin.Context) { user, ok := m.parseToken(c) if !ok { if c.GetBool(AccountBannedKey) { bannedJSON(c) return } c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } if model.RoleLevel(model.Role(user.Role)) < model.RoleLevel(model.RoleAdmin) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } c.Set("user", user) c.Next() } } // ActorKey 实时权限快照在 gin.Context 中的键 const ActorKey = "actor" // RequireStaff 必须是管理团队成员(板块管理员及以上), // 并把实时 Actor(角色+板块授权)写入 context 供 RequirePerm/handler 使用 func (m *AuthMiddleware) RequireStaff() gin.HandlerFunc { return func(c *gin.Context) { claims, ok := m.parseToken(c) if !ok { if c.GetBool(AccountBannedKey) { bannedJSON(c) return } c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } actor, err := m.auth.LoadActor(claims.ID) if err != nil || !actor.IsStaff() { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } c.Set("user", claims) c.Set(ActorKey, actor) c.Next() } } // RequirePerm 功能点鉴权,必须接在 RequireStaff 之后 func (m *AuthMiddleware) RequirePerm(perm string) gin.HandlerFunc { return func(c *gin.Context) { actor := CurrentActor(c) if !actor.HasPerm(perm) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限执行该操作"}) return } c.Next() } } // parseToken 解析并校验 token: // 1. 优先从 HttpOnly cookie 读取,回退 Authorization header // 2. 校验 JWT 签名和过期 // 3. 查 DB 实时校验 token_version 和 banned 状态 func (m *AuthMiddleware) parseToken(c *gin.Context) (*service.UserClaims, bool) { tokenStr, err := c.Cookie(service.CookieName) if err != nil || tokenStr == "" { auth := c.GetHeader("Authorization") if auth == "" { return nil, false } tokenStr = strings.TrimPrefix(auth, "Bearer ") if tokenStr == auth { return nil, false } } claims, err := m.auth.ParseToken(tokenStr) if err != nil { return nil, false } // 实时校验:token_version 匹配 + 未封禁(防止旧 JWT 在封禁/改密码后仍有效) if _, err := m.auth.ValidateClaims(claims); err != nil { // 封禁原因写入 context:401 与 403 的区分由上层中间件/handler 完成 if errors.Is(err, service.ErrAccountBanned) { c.Set(AccountBannedKey, true) } return nil, false } // 异步刷新在线心跳(SQL 每 60s 限频一次),不阻塞请求 go m.auth.TouchLastSeen(claims.ID) return claims, true } // CurrentUser 从 context 获取当前用户 func CurrentUser(c *gin.Context) *service.UserClaims { if v, ok := c.Get("user"); ok { if u, ok := v.(*service.UserClaims); ok { return u } } return nil } // CurrentActor 从 context 获取当前操作者的实时权限快照(RequireStaff 写入) func CurrentActor(c *gin.Context) *service.Actor { if v, ok := c.Get(ActorKey); ok { if a, ok := v.(*service.Actor); ok { return a } } return nil }