3.10.2 用户认证系统

3.10.2 用户认证系统 #

用户认证系统是 Web 应用的核心功能之一。本节将详细实现一个完整的用户认证系统,包括用户注册、登录、权限管理、密码重置等功能,并集成 JWT 认证和 RBAC 权限控制。

数据模型设计 #

用户相关模型 #

// internal/model/user.go
package model

import (
    "time"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

// User 用户模型
type User struct {
    ID          uint      `json:"id" gorm:"primaryKey"`
    Username    string    `json:"username" gorm:"uniqueIndex;size:50;not null"`
    Email       string    `json:"email" gorm:"uniqueIndex;size:100;not null"`
    Password    string    `json:"-" gorm:"size:255;not null"`
    Nickname    string    `json:"nickname" gorm:"size:50"`
    Avatar      string    `json:"avatar" gorm:"size:255"`
    Bio         string    `json:"bio" gorm:"size:500"`
    Status      UserStatus `json:"status" gorm:"default:1"`
    LastLoginAt *time.Time `json:"last_login_at"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`

    // 关联关系
    Roles    []Role    `json:"roles" gorm:"many2many:user_roles;"`
    Articles []Article `json:"articles" gorm:"foreignKey:AuthorID"`
    Comments []Comment `json:"comments" gorm:"foreignKey:UserID"`
}

// UserStatus 用户状态
type UserStatus int

const (
    UserStatusInactive UserStatus = 0 // 未激活
    UserStatusActive   UserStatus = 1 // 正常
    UserStatusBanned   UserStatus = 2 // 禁用
)

// Role 角色模型
type Role struct {
    ID          uint      `json:"id" gorm:"primaryKey"`
    Name        string    `json:"name" gorm:"uniqueIndex;size:50;not null"`
    DisplayName string    `json:"display_name" gorm:"size:100;not null"`
    Description string    `json:"description" gorm:"size:255"`
    IsActive    bool      `json:"is_active" gorm:"default:true"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`

    // 关联关系
    Users       []User       `json:"users" gorm:"many2many:user_roles;"`
    Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}

// Permission 权限模型
type Permission struct {
    ID          uint      `json:"id" gorm:"primaryKey"`
    Name        string    `json:"name" gorm:"uniqueIndex;size:100;not null"`
    DisplayName string    `json:"display_name" gorm:"size:100;not null"`
    Description string    `json:"description" gorm:"size:255"`
    Resource    string    `json:"resource" gorm:"size:50;not null"`
    Action      string    `json:"action" gorm:"size:50;not null"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`

    // 关联关系
    Roles []Role `json:"roles" gorm:"many2many:role_permissions;"`
}

// UserRole 用户角色关联
type UserRole struct {
    UserID    uint      `json:"user_id" gorm:"primaryKey"`
    RoleID    uint      `json:"role_id" gorm:"primaryKey"`
    CreatedAt time.Time `json:"created_at"`
}

// RolePermission 角色权限关联
type RolePermission struct {
    RoleID       uint      `json:"role_id" gorm:"primaryKey"`
    PermissionID uint      `json:"permission_id" gorm:"primaryKey"`
    CreatedAt    time.Time `json:"created_at"`
}

// BeforeCreate 创建前钩子
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Nickname == "" {
        u.Nickname = u.Username
    }
    return nil
}

// HashPassword 密码加密
func (u *User) HashPassword(password string) error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hashedPassword)
    return nil
}

// CheckPassword 验证密码
func (u *User) CheckPassword(password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
    return err == nil
}

// IsActive 检查用户是否激活
func (u *User) IsActive() bool {
    return u.Status == UserStatusActive
}

// HasRole 检查用户是否拥有指定角色
func (u *User) HasRole(roleName string) bool {
    for _, role := range u.Roles {
        if role.Name == roleName {
            return true
        }
    }
    return false
}

// HasPermission 检查用户是否拥有指定权限
func (u *User) HasPermission(resource, action string) bool {
    for _, role := range u.Roles {
        for _, permission := range role.Permissions {
            if permission.Resource == resource && permission.Action == action {
                return true
            }
            // 支持通配符权限
            if permission.Resource == "*" || permission.Action == "*" {
                return true
            }
        }
    }
    return false
}

认证相关模型 #

// internal/model/auth.go
package model

import (
    "time"
    "gorm.io/gorm"
)

// RefreshToken 刷新令牌
type RefreshToken struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    UserID    uint      `json:"user_id" gorm:"not null;index"`
    Token     string    `json:"token" gorm:"uniqueIndex;size:255;not null"`
    ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
    CreatedAt time.Time `json:"created_at"`

    // 关联关系
    User User `json:"user" gorm:"foreignKey:UserID"`
}

// IsExpired 检查令牌是否过期
func (rt *RefreshToken) IsExpired() bool {
    return time.Now().After(rt.ExpiresAt)
}

// PasswordReset 密码重置
type PasswordReset struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Email     string    `json:"email" gorm:"size:100;not null;index"`
    Token     string    `json:"token" gorm:"uniqueIndex;size:255;not null"`
    ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
    Used      bool      `json:"used" gorm:"default:false"`
    CreatedAt time.Time `json:"created_at"`
}

// IsExpired 检查重置令牌是否过期
func (pr *PasswordReset) IsExpired() bool {
    return time.Now().After(pr.ExpiresAt)
}

// EmailVerification 邮箱验证
type EmailVerification struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Email     string    `json:"email" gorm:"size:100;not null;index"`
    Token     string    `json:"token" gorm:"uniqueIndex;size:255;not null"`
    ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
    Used      bool      `json:"used" gorm:"default:false"`
    CreatedAt time.Time `json:"created_at"`
}

// IsExpired 检查验证令牌是否过期
func (ev *EmailVerification) IsExpired() bool {
    return time.Now().After(ev.ExpiresAt)
}

数据访问层 #

用户仓储 #

// internal/repository/user.go
package repository

import (
    "context"
    "blog-system/internal/model"
    "gorm.io/gorm"
)

// UserRepository 用户仓储接口
type UserRepository interface {
    Create(ctx context.Context, user *model.User) error
    GetByID(ctx context.Context, id uint) (*model.User, error)
    GetByUsername(ctx context.Context, username string) (*model.User, error)
    GetByEmail(ctx context.Context, email string) (*model.User, error)
    Update(ctx context.Context, user *model.User) error
    Delete(ctx context.Context, id uint) error
    List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error)
    AssignRole(ctx context.Context, userID, roleID uint) error
    RemoveRole(ctx context.Context, userID, roleID uint) error
    GetUserRoles(ctx context.Context, userID uint) ([]*model.Role, error)
    GetUserPermissions(ctx context.Context, userID uint) ([]*model.Permission, error)
}

// userRepository 用户仓储实现
type userRepository struct {
    BaseRepository
}

// Create 创建用户
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
    return r.db.WithContext(ctx).Create(user).Error
}

// GetByID 根据ID获取用户
func (r *userRepository) GetByID(ctx context.Context, id uint) (*model.User, error) {
    var user model.User
    err := r.db.WithContext(ctx).
        Preload("Roles.Permissions").
        First(&user, id).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

// GetByUsername 根据用户名获取用户
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*model.User, error) {
    var user model.User
    err := r.db.WithContext(ctx).
        Preload("Roles.Permissions").
        Where("username = ?", username).
        First(&user).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

// GetByEmail 根据邮箱获取用户
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
    var user model.User
    err := r.db.WithContext(ctx).
        Preload("Roles.Permissions").
        Where("email = ?", email).
        First(&user).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

// Update 更新用户
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
    return r.db.WithContext(ctx).Save(user).Error
}

// Delete 删除用户
func (r *userRepository) Delete(ctx context.Context, id uint) error {
    return r.db.WithContext(ctx).Delete(&model.User{}, id).Error
}

// List 获取用户列表
func (r *userRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
    var users []*model.User
    var total int64

    db := r.db.WithContext(ctx).Model(&model.User{})

    if err := db.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    err := db.Preload("Roles").
        Scopes(r.Paginate(page, pageSize)).
        Find(&users).Error

    return users, total, err
}

// AssignRole 分配角色
func (r *userRepository) AssignRole(ctx context.Context, userID, roleID uint) error {
    userRole := &model.UserRole{
        UserID: userID,
        RoleID: roleID,
    }
    return r.db.WithContext(ctx).Create(userRole).Error
}

// RemoveRole 移除角色
func (r *userRepository) RemoveRole(ctx context.Context, userID, roleID uint) error {
    return r.db.WithContext(ctx).
        Where("user_id = ? AND role_id = ?", userID, roleID).
        Delete(&model.UserRole{}).Error
}

// GetUserRoles 获取用户角色
func (r *userRepository) GetUserRoles(ctx context.Context, userID uint) ([]*model.Role, error) {
    var roles []*model.Role
    err := r.db.WithContext(ctx).
        Table("roles").
        Joins("JOIN user_roles ON roles.id = user_roles.role_id").
        Where("user_roles.user_id = ?", userID).
        Find(&roles).Error
    return roles, err
}

// GetUserPermissions 获取用户权限
func (r *userRepository) GetUserPermissions(ctx context.Context, userID uint) ([]*model.Permission, error) {
    var permissions []*model.Permission
    err := r.db.WithContext(ctx).
        Table("permissions").
        Joins("JOIN role_permissions ON permissions.id = role_permissions.permission_id").
        Joins("JOIN user_roles ON role_permissions.role_id = user_roles.role_id").
        Where("user_roles.user_id = ?", userID).
        Distinct().
        Find(&permissions).Error
    return permissions, err
}

认证仓储 #

// internal/repository/auth.go
package repository

import (
    "context"
    "blog-system/internal/model"
)

// AuthRepository 认证仓储接口
type AuthRepository interface {
    CreateRefreshToken(ctx context.Context, token *model.RefreshToken) error
    GetRefreshToken(ctx context.Context, token string) (*model.RefreshToken, error)
    DeleteRefreshToken(ctx context.Context, token string) error
    DeleteUserRefreshTokens(ctx context.Context, userID uint) error

    CreatePasswordReset(ctx context.Context, reset *model.PasswordReset) error
    GetPasswordReset(ctx context.Context, token string) (*model.PasswordReset, error)
    UsePasswordReset(ctx context.Context, token string) error

    CreateEmailVerification(ctx context.Context, verification *model.EmailVerification) error
    GetEmailVerification(ctx context.Context, token string) (*model.EmailVerification, error)
    UseEmailVerification(ctx context.Context, token string) error
}

// authRepository 认证仓储实现
type authRepository struct {
    BaseRepository
}

// CreateRefreshToken 创建刷新令牌
func (r *authRepository) CreateRefreshToken(ctx context.Context, token *model.RefreshToken) error {
    return r.db.WithContext(ctx).Create(token).Error
}

// GetRefreshToken 获取刷新令牌
func (r *authRepository) GetRefreshToken(ctx context.Context, token string) (*model.RefreshToken, error) {
    var refreshToken model.RefreshToken
    err := r.db.WithContext(ctx).
        Preload("User").
        Where("token = ?", token).
        First(&refreshToken).Error
    if err != nil {
        return nil, err
    }
    return &refreshToken, nil
}

// DeleteRefreshToken 删除刷新令牌
func (r *authRepository) DeleteRefreshToken(ctx context.Context, token string) error {
    return r.db.WithContext(ctx).
        Where("token = ?", token).
        Delete(&model.RefreshToken{}).Error
}

// DeleteUserRefreshTokens 删除用户所有刷新令牌
func (r *authRepository) DeleteUserRefreshTokens(ctx context.Context, userID uint) error {
    return r.db.WithContext(ctx).
        Where("user_id = ?", userID).
        Delete(&model.RefreshToken{}).Error
}

// CreatePasswordReset 创建密码重置
func (r *authRepository) CreatePasswordReset(ctx context.Context, reset *model.PasswordReset) error {
    return r.db.WithContext(ctx).Create(reset).Error
}

// GetPasswordReset 获取密码重置
func (r *authRepository) GetPasswordReset(ctx context.Context, token string) (*model.PasswordReset, error) {
    var reset model.PasswordReset
    err := r.db.WithContext(ctx).
        Where("token = ? AND used = false", token).
        First(&reset).Error
    if err != nil {
        return nil, err
    }
    return &reset, nil
}

// UsePasswordReset 使用密码重置
func (r *authRepository) UsePasswordReset(ctx context.Context, token string) error {
    return r.db.WithContext(ctx).
        Model(&model.PasswordReset{}).
        Where("token = ?", token).
        Update("used", true).Error
}

// CreateEmailVerification 创建邮箱验证
func (r *authRepository) CreateEmailVerification(ctx context.Context, verification *model.EmailVerification) error {
    return r.db.WithContext(ctx).Create(verification).Error
}

// GetEmailVerification 获取邮箱验证
func (r *authRepository) GetEmailVerification(ctx context.Context, token string) (*model.EmailVerification, error) {
    var verification model.EmailVerification
    err := r.db.WithContext(ctx).
        Where("token = ? AND used = false", token).
        First(&verification).Error
    if err != nil {
        return nil, err
    }
    return &verification, nil
}

// UseEmailVerification 使用邮箱验证
func (r *authRepository) UseEmailVerification(ctx context.Context, token string) error {
    return r.db.WithContext(ctx).
        Model(&model.EmailVerification{}).
        Where("token = ?", token).
        Update("used", true).Error
}

业务逻辑层 #

认证服务 #

// internal/service/auth.go
package service

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "time"

    "blog-system/internal/model"
    "blog-system/internal/repository"
    "blog-system/pkg/jwt"
    "blog-system/pkg/response"
    "gorm.io/gorm"
)

// AuthService 认证服务接口
type AuthService interface {
    Register(ctx context.Context, req *RegisterRequest) (*AuthResponse, error)
    Login(ctx context.Context, req *LoginRequest) (*AuthResponse, error)
    RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
    Logout(ctx context.Context, userID uint, refreshToken string) error
    ChangePassword(ctx context.Context, userID uint, req *ChangePasswordRequest) error
    ForgotPassword(ctx context.Context, email string) error
    ResetPassword(ctx context.Context, req *ResetPasswordRequest) error
    VerifyEmail(ctx context.Context, token string) error
    ResendVerification(ctx context.Context, email string) error
}

// authService 认证服务实现
type authService struct {
    BaseService
    userRepo repository.UserRepository
    authRepo repository.AuthRepository
    jwtManager *jwt.Manager
}

// NewAuthService 创建认证服务
func NewAuthService(
    userRepo repository.UserRepository,
    authRepo repository.AuthRepository,
    jwtManager *jwt.Manager,
) AuthService {
    return &authService{
        userRepo:   userRepo,
        authRepo:   authRepo,
        jwtManager: jwtManager,
    }
}

// RegisterRequest 注册请求
type RegisterRequest struct {
    Username string `json:"username" binding:"required,min=3,max=20" validate:"alphanum"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6,max=20"`
    Nickname string `json:"nickname" binding:"max=50"`
}

// LoginRequest 登录请求
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
    OldPassword string `json:"old_password" binding:"required"`
    NewPassword string `json:"new_password" binding:"required,min=6,max=20"`
}

// ResetPasswordRequest 重置密码请求
type ResetPasswordRequest struct {
    Token    string `json:"token" binding:"required"`
    Password string `json:"password" binding:"required,min=6,max=20"`
}

// AuthResponse 认证响应
type AuthResponse struct {
    User         *UserInfo `json:"user"`
    AccessToken  string    `json:"access_token"`
    RefreshToken string    `json:"refresh_token"`
    ExpiresAt    time.Time `json:"expires_at"`
}

// UserInfo 用户信息
type UserInfo struct {
    ID          uint                `json:"id"`
    Username    string              `json:"username"`
    Email       string              `json:"email"`
    Nickname    string              `json:"nickname"`
    Avatar      string              `json:"avatar"`
    Bio         string              `json:"bio"`
    Status      model.UserStatus    `json:"status"`
    Roles       []string            `json:"roles"`
    Permissions []string            `json:"permissions"`
    CreatedAt   time.Time           `json:"created_at"`
}

// Register 用户注册
func (s *authService) Register(ctx context.Context, req *RegisterRequest) (*AuthResponse, error) {
    // 检查用户名是否存在
    if _, err := s.userRepo.GetByUsername(ctx, req.Username); err == nil {
        return nil, response.ErrUserExists
    }

    // 检查邮箱是否存在
    if _, err := s.userRepo.GetByEmail(ctx, req.Email); err == nil {
        return nil, response.NewError(40002, "邮箱已被注册", "")
    }

    // 创建用户
    user := &model.User{
        Username: req.Username,
        Email:    req.Email,
        Nickname: req.Nickname,
        Status:   model.UserStatusInactive, // 需要邮箱验证
    }

    if err := user.HashPassword(req.Password); err != nil {
        return nil, response.ErrInternalServer
    }

    if err := s.userRepo.Create(ctx, user); err != nil {
        return nil, response.ErrInternalServer
    }

    // 分配默认角色
    if err := s.assignDefaultRole(ctx, user.ID); err != nil {
        s.logger.Error("分配默认角色失败", "user_id", user.ID, "error", err)
    }

    // 发送邮箱验证
    if err := s.sendEmailVerification(ctx, user.Email); err != nil {
        s.logger.Error("发送邮箱验证失败", "email", user.Email, "error", err)
    }

    // 生成令牌
    return s.generateAuthResponse(ctx, user)
}

// Login 用户登录
func (s *authService) Login(ctx context.Context, req *LoginRequest) (*AuthResponse, error) {
    // 获取用户
    user, err := s.userRepo.GetByUsername(ctx, req.Username)
    if err != nil {
        if err == gorm.ErrRecordNotFound {
            return nil, response.NewError(40001, "用户名或密码错误", "")
        }
        return nil, response.ErrInternalServer
    }

    // 验证密码
    if !user.CheckPassword(req.Password) {
        return nil, response.NewError(40001, "用户名或密码错误", "")
    }

    // 检查用户状态
    if user.Status == model.UserStatusBanned {
        return nil, response.NewError(40003, "账户已被禁用", "")
    }

    // 更新最后登录时间
    now := time.Now()
    user.LastLoginAt = &now
    if err := s.userRepo.Update(ctx, user); err != nil {
        s.logger.Error("更新最后登录时间失败", "user_id", user.ID, "error", err)
    }

    // 生成令牌
    return s.generateAuthResponse(ctx, user)
}

// RefreshToken 刷新令牌
func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
    // 获取刷新令牌
    token, err := s.authRepo.GetRefreshToken(ctx, refreshToken)
    if err != nil {
        return nil, response.NewError(40006, "无效的刷新令牌", "")
    }

    // 检查是否过期
    if token.IsExpired() {
        s.authRepo.DeleteRefreshToken(ctx, refreshToken)
        return nil, response.NewError(40007, "刷新令牌已过期", "")
    }

    // 检查用户状态
    if !token.User.IsActive() {
        return nil, response.NewError(40003, "账户已被禁用", "")
    }

    // 删除旧的刷新令牌
    s.authRepo.DeleteRefreshToken(ctx, refreshToken)

    // 生成新的令牌
    return s.generateAuthResponse(ctx, &token.User)
}

// Logout 用户登出
func (s *authService) Logout(ctx context.Context, userID uint, refreshToken string) error {
    // 删除刷新令牌
    if refreshToken != "" {
        s.authRepo.DeleteRefreshToken(ctx, refreshToken)
    } else {
        // 删除用户所有刷新令牌
        s.authRepo.DeleteUserRefreshTokens(ctx, userID)
    }
    return nil
}

// ChangePassword 修改密码
func (s *authService) ChangePassword(ctx context.Context, userID uint, req *ChangePasswordRequest) error {
    // 获取用户
    user, err := s.userRepo.GetByID(ctx, userID)
    if err != nil {
        return response.ErrUserNotFound
    }

    // 验证旧密码
    if !user.CheckPassword(req.OldPassword) {
        return response.NewError(40003, "原密码错误", "")
    }

    // 设置新密码
    if err := user.HashPassword(req.NewPassword); err != nil {
        return response.ErrInternalServer
    }

    // 更新用户
    if err := s.userRepo.Update(ctx, user); err != nil {
        return response.ErrInternalServer
    }

    // 删除所有刷新令牌,强制重新登录
    s.authRepo.DeleteUserRefreshTokens(ctx, userID)

    return nil
}

// ForgotPassword 忘记密码
func (s *authService) ForgotPassword(ctx context.Context, email string) error {
    // 检查用户是否存在
    user, err := s.userRepo.GetByEmail(ctx, email)
    if err != nil {
        // 为了安全,不暴露用户是否存在
        return nil
    }

    // 生成重置令牌
    token := s.generateToken()
    reset := &model.PasswordReset{
        Email:     email,
        Token:     token,
        ExpiresAt: time.Now().Add(1 * time.Hour), // 1小时有效期
    }

    if err := s.authRepo.CreatePasswordReset(ctx, reset); err != nil {
        return response.ErrInternalServer
    }

    // 发送重置邮件
    if err := s.sendPasswordResetEmail(ctx, user.Email, token); err != nil {
        s.logger.Error("发送密码重置邮件失败", "email", email, "error", err)
    }

    return nil
}

// ResetPassword 重置密码
func (s *authService) ResetPassword(ctx context.Context, req *ResetPasswordRequest) error {
    // 获取重置记录
    reset, err := s.authRepo.GetPasswordReset(ctx, req.Token)
    if err != nil {
        return response.NewError(40006, "无效的重置令牌", "")
    }

    // 检查是否过期
    if reset.IsExpired() {
        return response.NewError(40007, "重置令牌已过期", "")
    }

    // 获取用户
    user, err := s.userRepo.GetByEmail(ctx, reset.Email)
    if err != nil {
        return response.ErrUserNotFound
    }

    // 设置新密码
    if err := user.HashPassword(req.Password); err != nil {
        return response.ErrInternalServer
    }

    // 更新用户
    if err := s.userRepo.Update(ctx, user); err != nil {
        return response.ErrInternalServer
    }

    // 标记重置令牌为已使用
    s.authRepo.UsePasswordReset(ctx, req.Token)

    // 删除所有刷新令牌
    s.authRepo.DeleteUserRefreshTokens(ctx, user.ID)

    return nil
}

// VerifyEmail 验证邮箱
func (s *authService) VerifyEmail(ctx context.Context, token string) error {
    // 获取验证记录
    verification, err := s.authRepo.GetEmailVerification(ctx, token)
    if err != nil {
        return response.NewError(40006, "无效的验证令牌", "")
    }

    // 检查是否过期
    if verification.IsExpired() {
        return response.NewError(40007, "验证令牌已过期", "")
    }

    // 获取用户
    user, err := s.userRepo.GetByEmail(ctx, verification.Email)
    if err != nil {
        return response.ErrUserNotFound
    }

    // 激活用户
    user.Status = model.UserStatusActive
    if err := s.userRepo.Update(ctx, user); err != nil {
        return response.ErrInternalServer
    }

    // 标记验证令牌为已使用
    s.authRepo.UseEmailVerification(ctx, token)

    return nil
}

// ResendVerification 重新发送验证邮件
func (s *authService) ResendVerification(ctx context.Context, email string) error {
    // 检查用户是否存在
    user, err := s.userRepo.GetByEmail(ctx, email)
    if err != nil {
        return response.ErrUserNotFound
    }

    // 检查是否已经激活
    if user.Status == model.UserStatusActive {
        return response.NewError(40004, "邮箱已验证", "")
    }

    // 发送验证邮件
    return s.sendEmailVerification(ctx, email)
}

// generateAuthResponse 生成认证响应
func (s *authService) generateAuthResponse(ctx context.Context, user *model.User) (*AuthResponse, error) {
    // 生成访问令牌
    accessToken, expiresAt, err := s.jwtManager.GenerateToken(user.ID, user.Username, s.getUserRoles(user))
    if err != nil {
        return nil, response.ErrInternalServer
    }

    // 生成刷新令牌
    refreshTokenStr := s.generateToken()
    refreshToken := &model.RefreshToken{
        UserID:    user.ID,
        Token:     refreshTokenStr,
        ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30天
    }

    if err := s.authRepo.CreateRefreshToken(ctx, refreshToken); err != nil {
        return nil, response.ErrInternalServer
    }

    return &AuthResponse{
        User:         s.buildUserInfo(user),
        AccessToken:  accessToken,
        RefreshToken: refreshTokenStr,
        ExpiresAt:    expiresAt,
    }, nil
}

// buildUserInfo 构建用户信息
func (s *authService) buildUserInfo(user *model.User) *UserInfo {
    roles := make([]string, len(user.Roles))
    for i, role := range user.Roles {
        roles[i] = role.Name
    }

    permissions := make([]string, 0)
    permissionSet := make(map[string]bool)
    for _, role := range user.Roles {
        for _, permission := range role.Permissions {
            key := fmt.Sprintf("%s:%s", permission.Resource, permission.Action)
            if !permissionSet[key] {
                permissions = append(permissions, key)
                permissionSet[key] = true
            }
        }
    }

    return &UserInfo{
        ID:          user.ID,
        Username:    user.Username,
        Email:       user.Email,
        Nickname:    user.Nickname,
        Avatar:      user.Avatar,
        Bio:         user.Bio,
        Status:      user.Status,
        Roles:       roles,
        Permissions: permissions,
        CreatedAt:   user.CreatedAt,
    }
}

// getUserRoles 获取用户角色名称
func (s *authService) getUserRoles(user *model.User) []string {
    roles := make([]string, len(user.Roles))
    for i, role := range user.Roles {
        roles[i] = role.Name
    }
    return roles
}

// assignDefaultRole 分配默认角色
func (s *authService) assignDefaultRole(ctx context.Context, userID uint) error {
    // 这里假设有一个ID为1的"user"角色
    return s.userRepo.AssignRole(ctx, userID, 1)
}

// generateToken 生成随机令牌
func (s *authService) generateToken() string {
    bytes := make([]byte, 32)
    rand.Read(bytes)
    return hex.EncodeToString(bytes)
}

// sendEmailVerification 发送邮箱验证
func (s *authService) sendEmailVerification(ctx context.Context, email string) error {
    token := s.generateToken()
    verification := &model.EmailVerification{
        Email:     email,
        Token:     token,
        ExpiresAt: time.Now().Add(24 * time.Hour), // 24小时有效期
    }

    if err := s.authRepo.CreateEmailVerification(ctx, verification); err != nil {
        return err
    }

    // TODO: 实际发送邮件
    s.logger.Info("邮箱验证令牌", "email", email, "token", token)
    return nil
}

// sendPasswordResetEmail 发送密码重置邮件
func (s *authService) sendPasswordResetEmail(ctx context.Context, email, token string) error {
    // TODO: 实际发送邮件
    s.logger.Info("密码重置令牌", "email", email, "token", token)
    return nil
}

表现层 #

认证控制器 #

// internal/handler/auth.go
package handler

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "blog-system/internal/service"
    "blog-system/pkg/response"
)

// AuthHandler 认证处理器
type AuthHandler struct {
    BaseHandler
    authService service.AuthService
}

// NewAuthHandler 创建认证处理器
func NewAuthHandler(authService service.AuthService) *AuthHandler {
    return &AuthHandler{
        authService: authService,
    }
}

// Register 用户注册
// @Summary 用户注册
// @Description 用户注册接口
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.RegisterRequest true "注册信息"
// @Success 200 {object} response.Response{data=service.AuthResponse}
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
    var req service.RegisterRequest
    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    result, err := h.authService.Register(c.Request.Context(), &req)
    if err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "注册成功", result)
}

// Login 用户登录
// @Summary 用户登录
// @Description 用户登录接口
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.LoginRequest true "登录信息"
// @Success 200 {object} response.Response{data=service.AuthResponse}
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
    var req service.LoginRequest
    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    result, err := h.authService.Login(c.Request.Context(), &req)
    if err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "登录成功", result)
}

// RefreshToken 刷新令牌
// @Summary 刷新令牌
// @Description 刷新访问令牌
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body object{refresh_token=string} true "刷新令牌"
// @Success 200 {object} response.Response{data=service.AuthResponse}
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/refresh [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
    var req struct {
        RefreshToken string `json:"refresh_token" binding:"required"`
    }

    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    result, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken)
    if err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "令牌刷新成功", result)
}

// Logout 用户登出
// @Summary 用户登出
// @Description 用户登出接口
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body object{refresh_token=string} false "刷新令牌"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Security ApiKeyAuth
// @Router /api/v1/auth/logout [post]
func (h *AuthHandler) Logout(c *gin.Context) {
    userID, err := h.GetUserID(c)
    if err != nil {
        response.Error(c, response.ErrUnauthorized)
        return
    }

    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    c.ShouldBindJSON(&req)

    if err := h.authService.Logout(c.Request.Context(), userID, req.RefreshToken); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "登出成功", nil)
}

// ChangePassword 修改密码
// @Summary 修改密码
// @Description 修改用户密码
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.ChangePasswordRequest true "密码信息"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Security ApiKeyAuth
// @Router /api/v1/auth/change-password [post]
func (h *AuthHandler) ChangePassword(c *gin.Context) {
    userID, err := h.GetUserID(c)
    if err != nil {
        response.Error(c, response.ErrUnauthorized)
        return
    }

    var req service.ChangePasswordRequest
    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    if err := h.authService.ChangePassword(c.Request.Context(), userID, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "密码修改成功", nil)
}

// ForgotPassword 忘记密码
// @Summary 忘记密码
// @Description 发送密码重置邮件
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body object{email=string} true "邮箱地址"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/forgot-password [post]
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
    var req struct {
        Email string `json:"email" binding:"required,email"`
    }

    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    if err := h.authService.ForgotPassword(c.Request.Context(), req.Email); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "密码重置邮件已发送", nil)
}

// ResetPassword 重置密码
// @Summary 重置密码
// @Description 通过令牌重置密码
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body service.ResetPasswordRequest true "重置信息"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/reset-password [post]
func (h *AuthHandler) ResetPassword(c *gin.Context) {
    var req service.ResetPasswordRequest
    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    if err := h.authService.ResetPassword(c.Request.Context(), &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "密码重置成功", nil)
}

// VerifyEmail 验证邮箱
// @Summary 验证邮箱
// @Description 通过令牌验证邮箱
// @Tags 认证
// @Accept json
// @Produce json
// @Param token query string true "验证令牌"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/verify-email [get]
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
    token := c.Query("token")
    if token == "" {
        response.Error(c, response.NewError(http.StatusBadRequest, "缺少验证令牌", ""))
        return
    }

    if err := h.authService.VerifyEmail(c.Request.Context(), token); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "邮箱验证成功", nil)
}

// ResendVerification 重新发送验证邮件
// @Summary 重新发送验证邮件
// @Description 重新发送邮箱验证邮件
// @Tags 认证
// @Accept json
// @Produce json
// @Param request body object{email=string} true "邮箱地址"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Router /api/v1/auth/resend-verification [post]
func (h *AuthHandler) ResendVerification(c *gin.Context) {
    var req struct {
        Email string `json:"email" binding:"required,email"`
    }

    if err := h.BindAndValidate(c, &req); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    if err := h.authService.ResendVerification(c.Request.Context(), req.Email); err != nil {
        response.Error(c, err.(*response.Error))
        return
    }

    response.SuccessWithMessage(c, "验证邮件已发送", nil)
}

中间件 #

JWT 认证中间件 #

// internal/middleware/auth.go
package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "blog-system/pkg/jwt"
    "blog-system/pkg/response"
)

// AuthMiddleware JWT认证中间件
func AuthMiddleware(jwtManager *jwt.Manager) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从Header中获取Authorization
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            response.Error(c, response.ErrUnauthorized)
            c.Abort()
            return
        }

        // 检查Bearer前缀
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            response.Error(c, response.NewError(http.StatusUnauthorized, "令牌格式错误", ""))
            c.Abort()
            return
        }

        // 验证令牌
        claims, err := jwtManager.VerifyToken(parts[1])
        if err != nil {
            response.Error(c, response.NewError(http.StatusUnauthorized, "无效的令牌", err.Error()))
            c.Abort()
            return
        }

        // 将用户信息存储到上下文
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("roles", claims.Roles)
        c.Set("claims", claims)

        c.Next()
    }
}

// OptionalAuthMiddleware 可选认证中间件
func OptionalAuthMiddleware(jwtManager *jwt.Manager) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.Next()
            return
        }

        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.Next()
            return
        }

        claims, err := jwtManager.VerifyToken(parts[1])
        if err != nil {
            c.Next()
            return
        }

        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("roles", claims.Roles)
        c.Set("claims", claims)

        c.Next()
    }
}

路由配置 #

// internal/router/auth.go
package router

import (
    "github.com/gin-gonic/gin"
    "blog-system/internal/handler"
    "blog-system/internal/middleware"
)

// setupAuthRoutes 设置认证路由
func setupAuthRoutes(r *gin.RouterGroup, h *handler.Handlers, m *middleware.Middlewares) {
    auth := r.Group("/auth")
    {
        // 公开路由
        auth.POST("/register", h.Auth.Register)
        auth.POST("/login", h.Auth.Login)
        auth.POST("/refresh", h.Auth.RefreshToken)
        auth.POST("/forgot-password", h.Auth.ForgotPassword)
        auth.POST("/reset-password", h.Auth.ResetPassword)
        auth.GET("/verify-email", h.Auth.VerifyEmail)
        auth.POST("/resend-verification", h.Auth.ResendVerification)

        // 需要认证的路由
        authenticated := auth.Group("")
        authenticated.Use(m.Auth)
        {
            authenticated.POST("/logout", h.Auth.Logout)
            authenticated.POST("/change-password", h.Auth.ChangePassword)
        }
    }
}

总结 #

本节实现了一个完整的用户认证系统,包括:

  1. 完整的数据模型:用户、角色、权限的关联设计
  2. 安全的密码处理:bcrypt 加密存储
  3. JWT 令牌认证:无状态的认证机制
  4. 刷新令牌机制:提升用户体验
  5. 邮箱验证功能:确保邮箱有效性
  6. 密码重置功能:安全的密码找回
  7. RBAC 权限控制:灵活的权限管理
  8. 完善的错误处理:统一的错误响应

这个认证系统具有企业级的安全性和可扩展性,为后续的业务功能提供了坚实的基础。