package middleware import ( "context" "errors" "log/slog" "strings" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqldao" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/redisdao" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/app" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/lzcwyjwt" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" ) type ctxKey string const ( adminIDKey ctxKey = "adminID" ) const ( // adminRoleSuper 超级管理员角色(账号管理接口专用)。 adminRoleSuper int8 = 1 // adminStatusEnabled 启用状态。 adminStatusEnabled int8 = 1 ) // JWTAuth 管理员鉴权中间件: // 解析 Bearer token → Redis 会话校验(失败 fail-open)→ 数据库状态回查(禁用即拒绝)→ AdminID 注入 context。 func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { response := app.NewResponse(c) // 1. 从 Authorization 头取 token:Bearer xxx.yyy.zzz authHeader := c.GetHeader("Authorization") if authHeader == "" { response.ToErrorResponse(errcode.UnauthorizedTokenMissing) c.Abort() return } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("invalid Authorization format")) c.Abort() return } // 2. 解析 token claims, err := lzcwyjwt.ParseAccessToken(parts[1]) if err != nil { switch { case errors.Is(err, lzcwyjwt.ErrTokenExpired): response.ToErrorResponse(errcode.UnauthorizedTokenTimeout) case errors.Is(err, lzcwyjwt.ErrTokenWrongType): response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token type mismatch")) default: response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails(err.Error())) } c.Abort() return } // 2.5 校验 access token 是否仍在会话中(登出/失效后立即拒绝)。 sessionID, serr := redisdao.AuthAdminIDByAccess(c.Request.Context(), parts[1]) if serr != nil { if errors.Is(serr, redis.Nil) { response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token session invalid")) c.Abort() return } // Redis 其它错误降级放行(fail-open):优先保证可用性; // 被禁用/删除的管理员仍会被下方 DB 回查拒绝,不会因本降级放行而绕过。 slog.WarnContext(c.Request.Context(), "check admin token session failed, allow by fallback", "error", serr) } else if sessionID != claims.AdminID { response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token user mismatch")) c.Abort() return } // 3. 数据库回查管理员状态(禁用/删除立即生效)。 admin, dbErr := mysqldao.NewAdminDao().AdminGetByID(c.Request.Context(), claims.AdminID) if dbErr != nil { response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("管理员不存在")) c.Abort() return } if admin.Status != adminStatusEnabled { response.ToErrorResponse(errcode.AdminDisabled) c.Abort() return } // 4. 把 adminID 注入 context ctx := context.WithValue(c.Request.Context(), adminIDKey, claims.AdminID) c.Request = c.Request.WithContext(ctx) c.Next() } } // AdminSuperAuth 超级管理员权限校验(在 JWTAuth 之后使用)。 func AdminSuperAuth() gin.HandlerFunc { return func(c *gin.Context) { response := app.NewResponse(c) adminID, ok := GetAdminID(c.Request.Context()) if !ok { response.ToErrorResponse(errcode.Unauthorized) c.Abort() return } admin, err := mysqldao.NewAdminDao().AdminGetByID(c.Request.Context(), adminID) if err != nil { response.ToErrorResponse(errcode.AdminNotFound) c.Abort() return } if admin.Role != adminRoleSuper { response.ToErrorResponse(errcode.Forbidden.WithDetails("仅超级管理员可操作")) c.Abort() return } c.Next() } } // GetAdminID 从 context 取登录管理员 ID(在 handler 里用)。 func GetAdminID(ctx context.Context) (int64, bool) { uid, ok := ctx.Value(adminIDKey).(int64) return uid, ok }