// =========================================== // 多班级版班级管理系统 - Go 后端 // // 开发者: Canglan // 联系方式: admin@sea-studio.top // 版权归属: Sea Network Technology Studio // 许可证: Apache License 2.0 // // 版权所有 © Sea Network Technology Studio // =========================================== package middleware import ( "context" "strings" "github.com/gin-gonic/gin" "hz-gitea.sea-studio.top/canglan/SharedClassManager/internal/config" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/database" appJwt "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/jwt" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/logger" "hz-gitea.sea-studio.top/canglan/SharedClassManager/pkg/response" ) // 上下文 Key 常量 const ( CtxUserID = "user_id" CtxUsername = "username" CtxUserType = "user_type" CtxStudentID = "student_id" CtxRole = "role" CtxRealName = "real_name" CtxClassID = "class_id" ) // 公开路径(不需要认证) var publicPaths = map[string]bool{ "/": true, "/health": true, "/api/auth/login": true, } // RegisterPublicPath 注册额外的公开路径(需在路由初始化阶段调用) func RegisterPublicPath(path string) { publicPaths[path] = true } // AuthRequired JWT 认证中间件 func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path // 公开路径跳过 if publicPaths[path] { c.Next() return } cfg := config.AppConfig // 获取 Authorization header authHeader := c.GetHeader("Authorization") if authHeader == "" { response.Unauthorized(c, "缺少认证令牌") c.Abort() return } // 解析 Bearer Token parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { response.Unauthorized(c, "认证格式错误") c.Abort() return } tokenStr := parts[1] // 验证 JWT claims, err := appJwt.VerifyToken(tokenStr) if err != nil { logger.Sugared.Warnf("JWT 验证失败: path=%s, err=%v", path, err) response.Unauthorized(c, "令牌无效或已过期") c.Abort() return } // 验证 Redis 中的 Token ctx := context.Background() storedToken, err := database.GetUserToken(ctx, claims.UserID) if err != nil || storedToken != tokenStr { logger.Sugared.Warnf("Redis Token 不匹配: path=%s, user_id=%d", path, claims.UserID) // 主动清理 Redis 中的旧 Token,避免残留 if err == nil && storedToken != "" && storedToken != tokenStr { _ = database.DeleteUserToken(ctx, claims.UserID) } response.Unauthorized(c, "令牌已失效,请重新登录") c.Abort() return } // 刷新 Token 过期时间(空闲超时) _ = database.ExpireToken(ctx, claims.UserID, cfg.JWTIdleTimeoutMinutes) // 将用户信息写入 Gin 上下文 c.Set(CtxUserID, claims.UserID) c.Set(CtxUsername, claims.Username) c.Set(CtxUserType, claims.UserType) c.Set(CtxRealName, claims.RealName) if claims.StudentID != nil { c.Set(CtxStudentID, *claims.StudentID) } c.Set(CtxRole, claims.Role) if claims.ClassID != nil { c.Set(CtxClassID, *claims.ClassID) } logger.Sugared.Debugf("认证成功: %s %s, user_id=%d, username=%s", c.Request.Method, path, claims.UserID, claims.Username) c.Next() } } // RequireRole 角色权限中间件 func RequireRole(roles ...string) gin.HandlerFunc { roleSet := make(map[string]bool, len(roles)) for _, r := range roles { roleSet[r] = true } return func(c *gin.Context) { userType, _ := c.Get(CtxUserType) role, _ := c.Get(CtxRole) // 超级管理员直接通过 if userType == "super_admin" { c.Next() return } // 检查 user_type if ut, ok := userType.(string); ok && roleSet[ut] { c.Next() return } // 检查 role(admin_roles.role_type) if r, ok := role.(string); ok && roleSet[r] { c.Next() return } response.Forbidden(c, "权限不足") c.Abort() } } // GetUserID 从上下文获取用户 ID func GetUserID(c *gin.Context) int { if v, exists := c.Get(CtxUserID); exists { if id, ok := v.(int); ok { return id } } return 0 } // GetUsername 从上下文获取用户名 func GetUsername(c *gin.Context) string { if v, exists := c.Get(CtxUsername); exists { if s, ok := v.(string); ok { return s } } return "" } // GetUserType 从上下文获取用户类型 func GetUserType(c *gin.Context) string { if v, exists := c.Get(CtxUserType); exists { if s, ok := v.(string); ok { return s } } return "" } // GetRole 从上下文获取角色 func GetRole(c *gin.Context) string { if v, exists := c.Get(CtxRole); exists { if s, ok := v.(string); ok { return s } } return "" } // GetClassID 从上下文获取班级 ID func GetClassID(c *gin.Context) int { if v, exists := c.Get(CtxClassID); exists { if id, ok := v.(int); ok { return id } } return 0 } // GetStudentID 从上下文获取学生 ID func GetStudentID(c *gin.Context) int { if v, exists := c.Get(CtxStudentID); exists { if id, ok := v.(int); ok { return id } } return 0 } // GetRealName 从上下文获取真实姓名 func GetRealName(c *gin.Context) string { if v, exists := c.Get(CtxRealName); exists { if s, ok := v.(string); ok { return s } } return "" }