Files
go_blog/handlers/auth.go
T
kevin f307781f58 docs: 全部 Go 代码注释汉化
- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文
- 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等
- 代码、字符串字面量、日志消息保持英文原文,零逻辑改动
- go build/vet 通过,go test -count=1 ./... 全绿
2026-08-27 19:03:03 +08:00

231 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "/")
}
}