package middleware import ( "net/http" "strings" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "gorm.io/gorm" "go_blog/i18n" "go_blog/models" ) // isAPIRequest 报告请求是否命中 /api 前缀的 JSON 接口。 // 认证失败时 API 返回 JSON 错误,页面则保持 302 重定向。 func isAPIRequest(c *gin.Context) bool { return strings.HasPrefix(c.Request.URL.Path, "/api") } // apiAuthError 以 API 错误格式终止请求(401 未认证 / 403 无权限)。 // 文案按请求语言翻译(SetUserContext 已在全局中间件中注入 tr)。 func apiAuthError(c *gin.Context, status int, trKey string) { tr, _ := c.Get("tr") m, _ := tr.(map[string]string) code := trKey msg := m[code] if msg == "" { code = "api_error" msg = m["api_error"] } c.AbortWithStatusJSON(status, gin.H{ "ok": false, "code": code, "error": msg, }) } // sessionUserID 从会话中提取已登录用户的数值 ID, // 兼容 int/uint/int64/float64 的存储类型。若不存在或类型不符,ok=false。 func sessionUserID(session sessions.Session) (uint, bool) { userID := session.Get("user_id") if userID == nil { return 0, false } switch v := userID.(type) { case uint: return v, true case int: return uint(v), true case int64: return uint(v), true case float64: return uint(v), true default: return 0, false } } // clearUserSession 从会话中清除认证状态,仅保留无害的 UI 偏好 // (语言与 CSRF 令牌,与登录处理器的轮换逻辑保持一致), // 以确保其他标签页中已渲染的表单仍然有效。 func clearUserSession(session sessions.Session) { lang, _ := session.Get("lang").(string) csrfTok, _ := session.Get(CSRFSessionKey).(string) session.Clear() if lang != "" { session.Set("lang", lang) } if csrfTok != "" { session.Set(CSRFSessionKey, csrfTok) } session.Save() } // AuthRequired 是保护路由的中间件。若用户未登录,则重定向到 /login。 // 会话用户还会在每次请求时重新对数据库校验:已停用、已锁定或已软删除的 // 账户会立即失去访问权限,而无需等到 Cookie 过期(SECURITY_TODO #20)。 func AuthRequired(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) uid, ok := sessionUserID(session) if !ok { if isAPIRequest(c) { apiAuthError(c, http.StatusUnauthorized, "api_unauthorized") return } c.Redirect(http.StatusFound, "/login") c.Abort() return } var user models.User if err := db.First(&user, uid).Error; err != nil || user.Status != models.StatusNormal { // 账户已不可用——销毁会话,防止过期 Cookie 被重放。 clearUserSession(session) if isAPIRequest(c) { apiAuthError(c, http.StatusUnauthorized, "api_unauthorized") return } c.Redirect(http.StatusFound, "/login") c.Abort() return } c.Next() } } // AdminRequired 是仅允许管理员角色用户访问路由的中间件。它必须在 // AuthRequired 之后运行(后者保证会话用户存在且状态正常)。 // 非管理员用户会被重定向回管理后台。 func AdminRequired(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) uid, ok := sessionUserID(session) if !ok { if isAPIRequest(c) { apiAuthError(c, http.StatusUnauthorized, "api_unauthorized") return } c.Redirect(http.StatusFound, "/login") c.Abort() return } var user models.User if err := db.First(&user, uid).Error; err != nil || user.Role != models.RoleAdmin { if isAPIRequest(c) { apiAuthError(c, http.StatusForbidden, "api_forbidden") return } c.Redirect(http.StatusFound, "/admin") c.Abort() return } c.Next() } } // SetUserContext 是全局中间件,读取会话并为所有页面设置 // 便于模板使用的上下文值(语言、认证状态等)。 func SetUserContext(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) // --- 语言检测 --- // 优先级:查询参数 > 会话 > Accept-Language 请求头 > 默认 EN var lang i18n.Lang queryLang := c.Query("lang") switch queryLang { case "zh": lang = i18n.ZH case "en": lang = i18n.EN case "": // 尝试从会话中读取 if saved, ok := session.Get("lang").(string); ok { lang = i18n.Lang(saved) } if lang == "" { // 尝试从 Accept-Language 请求头检测 lang = i18n.DetectLang(c.GetHeader("Accept-Language")) } default: // 查询参数包含不支持的语言——回退到英语。 lang = i18n.EN } // 将会话中的语言持久化。 session.Set("lang", string(lang)) session.Save() // 将翻译字典放入 Gin 上下文。 c.Set("tr", i18n.T(lang)) c.Set("lang", string(lang)) // 为语言切换链接设置相反的语言代码。 switchLang := "zh" if lang == i18n.ZH { switchLang = "en" } c.Set("switch_lang", switchLang) // --- 认证状态 --- // 仅当账户仍然存在且状态正常时,才认为用户已登录: // 被停用/锁定/软删除的账户,在其会话失效后 // 不得继续保留模板级权限(如评论自动通过)(SECURITY_TODO #20)。 isLoggedIn := false var username string var avatar string var displayName string var role string if uid, ok := sessionUserID(session); ok { var user models.User if err := db.First(&user, uid).Error; err == nil && user.Status == models.StatusNormal { isLoggedIn = true username = user.Username avatar = user.Avatar displayName = user.DisplayName role = user.Role } } c.Set("is_logged_in", isLoggedIn) c.Set("username", username) c.Set("avatar", avatar) c.Set("display_name", displayName) c.Set("role", role) // --- 站点平台配置(来自数据库缓存)--- site := models.GetSiteSetting() c.Set("site_setting", site) c.Set("site_logo", site.Logo) c.Set("site_logo_is_url", site.LogoIsURL()) c.Set("site_favicon", site.Favicon) c.Set("site_favicon_is_url", site.FaviconIsURL()) c.Set("site_logo_text", site.LogoText(string(lang))) c.Set("site_header_text", site.HeaderText(string(lang))) c.Set("site_home_welcome", site.HomeWelcome(string(lang))) c.Set("site_home_subtitle", site.HomeSubtitle(string(lang))) c.Set("site_footer_text", site.FooterText(string(lang))) // --- 导航链接(来自数据库缓存)--- navLinks := models.GetNavLinks() c.Set("nav_links", navLinks) c.Next() } }