package handlers import ( "net/http" "strings" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "go_blog/models" ) // LoginPage 渲染登录表单。 func LoginPage() gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) data := DefaultData(c) data["Title"] = tr["page_login"] if c.Query("error") == "1" { data["Error"] = tr["login_error"] } if c.Query("error") == "locked" { data["Error"] = tr["login_locked"] } // 根据站点设置检查是否允许注册 siteSetting, _ := c.Get("site_setting") if s, ok := siteSetting.(*models.SiteSetting); ok && s != nil { data["AllowRegistration"] = s.AllowRegistration } c.HTML(http.StatusOK, "login", data) } } // loginRequest 是 POST /api/auth/login 的 JSON 请求体。 type loginRequest struct { Username string `json:"username"` Password string `json:"password"` } // Login 处理登录表单提交。它对每个 IP+用户名实施速率限制 // (SECURITY_TODO #10),对于不存在的用户名会执行一次虚拟 bcrypt 比较, // 使耗时不会暴露用户名是否存在(SECURITY_TODO #25)。 func Login(db *gorm.DB, limiter *LoginRateLimiter) gin.HandlerFunc { return func(c *gin.Context) { var req loginRequest if !bindJSON(c, &req) { return } username := req.Username password := req.Password key := GetClientIP(c) + "\x00" + username if !limiter.Allow(key) { APIError(c, http.StatusTooManyRequests, "login_locked") return } var user models.User if err := db.Where("username = ?", username).First(&user).Error; err != nil { // 常量时间:失败前执行与真实密码校验等量的工作(bcrypt 比较), // 使耗时不会暴露用户名是否存在。 limiter.Fail(key) _ = bcrypt.CompareHashAndPassword(dummyHash, []byte(password)) APIError(c, http.StatusUnauthorized, "login_error") return } if !user.CheckPassword(password) { limiter.Fail(key) APIError(c, http.StatusUnauthorized, "login_error") return } // 拒绝非正常状态账户登录(已禁用 / 已锁定 / 未激活)。 if user.Status != models.StatusNormal { APIError(c, http.StatusUnauthorized, "login_error") return } // 成功:重置此键的失败计数。 limiter.Reset(key) // 权限变更时轮换会话,防止会话固定攻击: // 丢弃全部登录前状态,仅保留无害的 UI 偏好(语言和 CSRF 令牌, // 使其他标签页中已渲染的表单仍然有效)。 session := sessions.Default(c) lang, _ := session.Get("lang").(string) csrfTok, _ := session.Get("csrf_token").(string) session.Clear() if lang != "" { session.Set("lang", lang) } if csrfTok != "" { session.Set("csrf_token", csrfTok) } session.Set("user_id", user.ID) session.Set("username", user.Username) if err := session.Save(); err != nil { APIError(c, http.StatusInternalServerError, "api_error") return } // 根据用户角色跳转:管理员到 /admin,其他用户到首页 redirect := "/" if user.Role == models.RoleAdmin { redirect = "/admin" } APIOK(c, redirect, nil) } } // Logout 清除会话并返回跳转首页指令,由前端 fetch 发起跳转。 func Logout() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) session.Clear() session.Save() APIOK(c, "/", nil) } } // RegisterPage 渲染注册表单(仅在启用注册时可用)。 func RegisterPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 检查是否允许注册 var s models.SiteSetting if err := db.First(&s, 1).Error; err != nil || !s.AllowRegistration { c.Redirect(http.StatusFound, "/login") return } tr := getTr(c) data := DefaultData(c) data["Title"] = tr["page_register"] if errMsg := c.Query("error"); errMsg != "" { data["Error"] = tr[errMsg] } c.HTML(http.StatusOK, "register", data) } } // registerRequest 是 POST /api/auth/register 的 JSON 请求体。 type registerRequest struct { Username string `json:"username"` Password string `json:"password"` ConfirmPassword string `json:"confirm_password"` Email string `json:"email"` DisplayName string `json:"display_name"` } // Register 处理注册表单提交。 func Register(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 检查是否允许注册 var s models.SiteSetting if err := db.First(&s, 1).Error; err != nil || !s.AllowRegistration { APIError(c, http.StatusForbidden, "registration_disabled") return } var req registerRequest if !bindJSON(c, &req) { return } username := strings.TrimSpace(req.Username) password := req.Password confirmPassword := req.ConfirmPassword email := strings.TrimSpace(req.Email) displayName := strings.TrimSpace(req.DisplayName) // 校验输入 if username == "" || password == "" { APIError(c, http.StatusBadRequest, "register_required") return } if len(username) < 3 || len(username) > 32 { APIError(c, http.StatusBadRequest, "register_username_length") return } if len(password) < 6 { APIError(c, http.StatusBadRequest, "register_password_length") return } if password != confirmPassword { APIError(c, http.StatusBadRequest, "register_password_mismatch") return } // SECURITY (#24):拒绝格式非法的邮箱地址(可选字段)。 if !validateEmail(email) { APIError(c, http.StatusBadRequest, "register_email_invalid") return } // 检查用户名是否已存在 var existingUser models.User if err := db.Where("username = ?", username).First(&existingUser).Error; err == nil { APIError(c, http.StatusConflict, "user_username_exists") return } // 创建新用户 user := models.User{ Username: username, Email: email, DisplayName: displayName, Role: models.RoleAuthor, Status: models.StatusNormal, } if displayName == "" { user.DisplayName = username } if err := user.SetPassword(password); err != nil { APIError(c, http.StatusInternalServerError, "register_error") return } if err := db.Create(&user).Error; err != nil { APIError(c, http.StatusInternalServerError, "register_error") return } // 注册成功后自动登录(与登录处理器一致的会话轮换)。 session := sessions.Default(c) lang, _ := session.Get("lang").(string) csrfTok, _ := session.Get("csrf_token").(string) session.Clear() if lang != "" { session.Set("lang", lang) } if csrfTok != "" { session.Set("csrf_token", csrfTok) } session.Set("user_id", user.ID) session.Set("username", user.Username) if err := session.Save(); err != nil { APIError(c, http.StatusInternalServerError, "api_error") return } // 首页 APIOK(c, "/", nil) } }