- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文 - 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等 - 代码、字符串字面量、日志消息保持英文原文,零逻辑改动 - go build/vet 通过,go test -count=1 ./... 全绿
231 lines
6.2 KiB
Go
231 lines
6.2 KiB
Go
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)
|
||
}
|
||
}
|
||
|
||
// Login 处理登录表单提交。它对每个 IP+用户名实施速率限制
|
||
// (SECURITY_TODO #10),对于不存在的用户名会执行一次虚拟 bcrypt 比较,
|
||
// 使耗时不会暴露用户名是否存在(SECURITY_TODO #25)。
|
||
func Login(db *gorm.DB, limiter *loginRateLimiter) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
username := c.PostForm("username")
|
||
password := c.PostForm("password")
|
||
|
||
key := GetClientIP(c) + "\x00" + username
|
||
if !limiter.Allow(key) {
|
||
c.Redirect(http.StatusFound, "/login?error=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))
|
||
c.Redirect(http.StatusFound, "/login?error=1")
|
||
return
|
||
}
|
||
|
||
if !user.CheckPassword(password) {
|
||
limiter.Fail(key)
|
||
c.Redirect(http.StatusFound, "/login?error=1")
|
||
return
|
||
}
|
||
|
||
// 拒绝非正常状态账户登录(已禁用 / 已锁定 / 未激活)。
|
||
if user.Status != models.StatusNormal {
|
||
c.Redirect(http.StatusFound, "/login?error=1")
|
||
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 {
|
||
c.String(http.StatusInternalServerError, "Failed to save session")
|
||
return
|
||
}
|
||
|
||
// 根据用户角色重定向:管理员到 /admin,其他用户到首页
|
||
if user.Role == models.RoleAdmin {
|
||
c.Redirect(http.StatusFound, "/admin")
|
||
} else {
|
||
c.Redirect(http.StatusFound, "/")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Logout 清除会话并重定向回首页。
|
||
func Logout() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
session := sessions.Default(c)
|
||
session.Clear()
|
||
session.Save()
|
||
c.Redirect(http.StatusFound, "/")
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
c.Redirect(http.StatusFound, "/login")
|
||
return
|
||
}
|
||
|
||
username := strings.TrimSpace(c.PostForm("username"))
|
||
password := c.PostForm("password")
|
||
confirmPassword := c.PostForm("confirm_password")
|
||
email := strings.TrimSpace(c.PostForm("email"))
|
||
displayName := strings.TrimSpace(c.PostForm("display_name"))
|
||
|
||
// 校验输入
|
||
if username == "" || password == "" {
|
||
c.Redirect(http.StatusFound, "/register?error=register_required")
|
||
return
|
||
}
|
||
|
||
if len(username) < 3 || len(username) > 32 {
|
||
c.Redirect(http.StatusFound, "/register?error=register_username_length")
|
||
return
|
||
}
|
||
|
||
if len(password) < 6 {
|
||
c.Redirect(http.StatusFound, "/register?error=register_password_length")
|
||
return
|
||
}
|
||
|
||
if password != confirmPassword {
|
||
c.Redirect(http.StatusFound, "/register?error=register_password_mismatch")
|
||
return
|
||
}
|
||
|
||
// SECURITY (#24):拒绝格式非法的邮箱地址(可选字段)。
|
||
if !validateEmail(email) {
|
||
c.Redirect(http.StatusFound, "/register?error=register_email_invalid")
|
||
return
|
||
}
|
||
|
||
// 检查用户名是否已存在
|
||
var existingUser models.User
|
||
if err := db.Where("username = ?", username).First(&existingUser).Error; err == nil {
|
||
c.Redirect(http.StatusFound, "/register?error=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 {
|
||
c.Redirect(http.StatusFound, "/register?error=register_error")
|
||
return
|
||
}
|
||
|
||
if err := db.Create(&user).Error; err != nil {
|
||
c.Redirect(http.StatusFound, "/register?error=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 {
|
||
c.Redirect(http.StatusFound, "/login")
|
||
return
|
||
}
|
||
|
||
// 重定向到首页
|
||
c.Redirect(http.StatusFound, "/")
|
||
}
|
||
}
|