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)
}
}
}
总结 #
本节实现了一个完整的用户认证系统,包括:
- 完整的数据模型:用户、角色、权限的关联设计
- 安全的密码处理:bcrypt 加密存储
- JWT 令牌认证:无状态的认证机制
- 刷新令牌机制:提升用户体验
- 邮箱验证功能:确保邮箱有效性
- 密码重置功能:安全的密码找回
- RBAC 权限控制:灵活的权限管理
- 完善的错误处理:统一的错误响应
这个认证系统具有企业级的安全性和可扩展性,为后续的业务功能提供了坚实的基础。