Merge branch 'develop' into feature/auth-management-system

This commit is contained in:
2026-03-01 11:56:28 +08:00
4 changed files with 1043 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
package models
import (
"time"
)
// AuthToken 认证令牌模型
type AuthToken struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"not null;index" json:"tenant_id"`
UserID uint `gorm:"not null;index" json:"user_id"`
// 令牌信息
Token string `gorm:"size:512;not null;uniqueIndex" json:"token"`
TokenType string `gorm:"size:20;not null" json:"token_type"` // access, refresh
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
// 设备信息
DeviceID string `gorm:"size:100" json:"device_id"`
DeviceName string `gorm:"size:100" json:"device_name"`
DeviceType string `gorm:"size:50" json:"device_type"` // web, mobile, desktop
IPAddress string `gorm:"size:45" json:"ip_address"`
UserAgent string `gorm:"type:text" json:"user_agent"`
// 状态
IsRevoked bool `gorm:"default:false" json:"is_revoked"`
RevokedAt *time.Time `json:"revoked_at"`
RevokedReason string `gorm:"size:200" json:"revoked_reason"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
Tenant Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// LoginAttempt 登录尝试记录
type LoginAttempt struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"not null;index" json:"tenant_id"`
UserID *uint `gorm:"index" json:"user_id"`
// 尝试信息
Username string `gorm:"size:100" json:"username"`
Email string `gorm:"size:100" json:"email"`
IPAddress string `gorm:"size:45;not null" json:"ip_address"`
UserAgent string `gorm:"type:text" json:"user_agent"`
// 结果
Success bool `gorm:"not null" json:"success"`
FailureReason string `gorm:"size:200" json:"failure_reason"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
// 关联
Tenant Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"`
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// PasswordReset 密码重置请求
type PasswordReset struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"not null;index" json:"tenant_id"`
UserID uint `gorm:"not null;index" json:"user_id"`
// 重置信息
Token string `gorm:"size:100;not null;uniqueIndex" json:"token"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
// 状态
IsUsed bool `gorm:"default:false" json:"is_used"`
UsedAt *time.Time `json:"used_at"`
IPAddress string `gorm:"size:45" json:"ip_address"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
// 关联
Tenant Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
// Session 用户会话
type Session struct {
ID uint `gorm:"primaryKey" json:"id"`
TenantID uint `gorm:"not null;index" json:"tenant_id"`
UserID uint `gorm:"not null;index" json:"user_id"`
// 会话信息
SessionID string `gorm:"size:100;not null;uniqueIndex" json:"session_id"`
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
// 设备信息
DeviceID string `gorm:"size:100" json:"device_id"`
DeviceName string `gorm:"size:100" json:"device_name"`
DeviceType string `gorm:"size:50" json:"device_type"`
IPAddress string `gorm:"size:45" json:"ip_address"`
UserAgent string `gorm:"type:text" json:"user_agent"`
// 活动信息
LastActivity time.Time `json:"last_activity"`
IsActive bool `gorm:"default:true" json:"is_active"`
// 时间戳
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联
Tenant Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}

View File

@@ -0,0 +1,425 @@
package services
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
"smart-customer-service/config"
"smart-customer-service/internal/models"
"smart-customer-service/pkg/database"
"smart-customer-service/pkg/jwt"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type AuthService struct {
cfg *config.Config
db *gorm.DB
jwt *jwt.JWTManager
}
func NewAuthService(cfg *config.Config, db *gorm.DB) *AuthService {
return &AuthService{
cfg: cfg,
db: db,
jwt: jwt.NewJWTManager(cfg.JWT.Secret),
}
}
// LoginRequest 登录请求
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
}
// LoginResponse 登录响应
type LoginResponse struct {
User *models.User `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
TokenType string `json:"token_type"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
TenantID uint `json:"tenant_id"`
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"full_name"`
Phone string `json:"phone"`
}
// RegisterResponse 注册响应
type RegisterResponse struct {
User *models.User `json:"user"`
Token string `json:"token"`
}
// Login 用户登录
func (s *AuthService) Login(req *LoginRequest) (*LoginResponse, error) {
// 记录登录尝试
loginAttempt := &models.LoginAttempt{
Email: req.Email,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
Success: false,
}
defer s.recordLoginAttempt(loginAttempt)
// 查找用户
var user models.User
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
loginAttempt.FailureReason = "用户不存在"
return nil, errors.New("用户名或密码错误")
}
loginAttempt.FailureReason = "数据库错误"
return nil, err
}
// 检查用户状态
if user.Status != "active" {
loginAttempt.FailureReason = "用户状态异常: " + user.Status
return nil, errors.New("用户账户不可用")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
loginAttempt.UserID = &user.ID
loginAttempt.FailureReason = "密码错误"
return nil, errors.New("用户名或密码错误")
}
// 更新最后登录时间
now := time.Now()
user.LastLoginAt = &now
user.LastIP = req.IPAddress
if err := s.db.Save(&user).Error; err != nil {
loginAttempt.FailureReason = "更新登录信息失败"
return nil, err
}
// 生成JWT令牌
accessToken, accessExpiresAt, err := s.jwt.GenerateAccessToken(user.ID, user.TenantID, user.Role)
if err != nil {
loginAttempt.FailureReason = "生成令牌失败"
return nil, err
}
refreshToken, refreshExpiresAt, err := s.jwt.GenerateRefreshToken(user.ID, user.TenantID)
if err != nil {
loginAttempt.FailureReason = "生成刷新令牌失败"
return nil, err
}
// 保存令牌到数据库
authToken := &models.AuthToken{
TenantID: user.TenantID,
UserID: user.ID,
Token: refreshToken,
TokenType: "refresh",
ExpiresAt: refreshExpiresAt,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceType: req.DeviceType,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
}
if err := s.db.Create(authToken).Error; err != nil {
loginAttempt.FailureReason = "保存令牌失败"
return nil, err
}
// 创建会话
session := &models.Session{
TenantID: user.TenantID,
UserID: user.ID,
SessionID: generateSessionID(),
ExpiresAt: refreshExpiresAt,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceType: req.DeviceType,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
LastActivity: now,
}
if err := s.db.Create(session).Error; err != nil {
// 不影响主要登录流程,只记录日志
fmt.Printf("创建会话失败: %v\n", err)
}
// 标记登录尝试为成功
loginAttempt.UserID = &user.ID
loginAttempt.Success = true
loginAttempt.FailureReason = ""
s.recordLoginAttempt(loginAttempt)
return &LoginResponse{
User: &user,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: accessExpiresAt,
TokenType: "Bearer",
}, nil
}
// Register 用户注册
func (s *AuthService) Register(req *RegisterRequest) (*RegisterResponse, error) {
// 检查邮箱是否已存在
var existingUser models.User
if err := s.db.Where("email = ? AND tenant_id = ?", req.Email, req.TenantID).First(&existingUser).Error; err == nil {
return nil, errors.New("邮箱已被注册")
}
// 检查用户名是否已存在
if err := s.db.Where("username = ? AND tenant_id = ?", req.Username, req.TenantID).First(&existingUser).Error; err == nil {
return nil, errors.New("用户名已被使用")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 创建用户
user := &models.User{
TenantID: req.TenantID,
Username: req.Username,
Email: req.Email,
Password: string(hashedPassword),
FullName: req.FullName,
Phone: req.Phone,
Role: "user",
Status: "active",
IsVerified: false,
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
// 生成验证令牌(简化版,实际应发送验证邮件)
verificationToken := generateRandomToken(32)
return &RegisterResponse{
User: user,
Token: verificationToken,
}, nil
}
// RefreshToken 刷新访问令牌
func (s *AuthService) RefreshToken(refreshToken string) (*LoginResponse, error) {
// 查找刷新令牌
var authToken models.AuthToken
if err := s.db.Where("token = ? AND token_type = 'refresh' AND is_revoked = false", refreshToken).First(&authToken).Error; err != nil {
return nil, errors.New("无效的刷新令牌")
}
// 检查令牌是否过期
if time.Now().After(authToken.ExpiresAt) {
// 标记令牌为已撤销
now := time.Now()
authToken.IsRevoked = true
authToken.RevokedAt = &now
authToken.RevokedReason = "令牌过期"
s.db.Save(&authToken)
return nil, errors.New("刷新令牌已过期")
}
// 查找用户
var user models.User
if err := s.db.First(&user, authToken.UserID).Error; err != nil {
return nil, errors.New("用户不存在")
}
// 检查用户状态
if user.Status != "active" {
return nil, errors.New("用户账户不可用")
}
// 生成新的访问令牌
accessToken, expiresAt, err := s.jwt.GenerateAccessToken(user.ID, user.TenantID, user.Role)
if err != nil {
return nil, err
}
return &LoginResponse{
User: &user,
AccessToken: accessToken,
ExpiresAt: expiresAt,
TokenType: "Bearer",
}, nil
}
// Logout 用户登出
func (s *AuthService) Logout(token string, userID uint) error {
// 撤销刷新令牌
var authToken models.AuthToken
if err := s.db.Where("token = ? AND user_id = ?", token, userID).First(&authToken).Error; err != nil {
return nil // 令牌不存在,无需处理
}
now := time.Now()
authToken.IsRevoked = true
authToken.RevokedAt = &now
authToken.RevokedReason = "用户主动登出"
return s.db.Save(&authToken).Error
}
// ValidateToken 验证访问令牌
func (s *AuthService) ValidateToken(token string) (*jwt.Claims, error) {
return s.jwt.ValidateToken(token)
}
// ChangePassword 修改密码
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
// 验证旧密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)); err != nil {
return errors.New("旧密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
user.Password = string(hashedPassword)
return s.db.Save(&user).Error
}
// RequestPasswordReset 请求密码重置
func (s *AuthService) RequestPasswordReset(email string, tenantID uint) (string, error) {
var user models.User
if err := s.db.Where("email = ? AND tenant_id = ?", email, tenantID).First(&user).Error; err != nil {
// 出于安全考虑,即使用户不存在也返回成功
return "", nil
}
// 生成重置令牌
token := generateRandomToken(32)
expiresAt := time.Now().Add(24 * time.Hour) // 24小时有效
resetRequest := &models.PasswordReset{
TenantID: tenantID,
UserID: user.ID,
Token: token,
ExpiresAt: expiresAt,
}
if err := s.db.Create(resetRequest).Error; err != nil {
return "", err
}
return token, nil
}
// ResetPassword 重置密码
func (s *AuthService) ResetPassword(token, newPassword string) error {
var resetRequest models.PasswordReset
if err := s.db.Where("token = ? AND is_used = false", token).First(&resetRequest).Error; err != nil {
return errors.New("无效的重置令牌")
}
// 检查令牌是否过期
if time.Now().After(resetRequest.ExpiresAt) {
return errors.New("重置令牌已过期")
}
// 查找用户
var user models.User
if err := s.db.First(&user, resetRequest.UserID).Error; err != nil {
return errors.New("用户不存在")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
// 更新密码
user.Password = string(hashedPassword)
if err := s.db.Save(&user).Error; err != nil {
return err
}
// 标记重置令牌为已使用
now := time.Now()
resetRequest.IsUsed = true
resetRequest.UsedAt = &now
return s.db.Save(&resetRequest).Error
}
// 辅助函数
func (s *AuthService) recordLoginAttempt(attempt *models.LoginAttempt) {
if err := s.db.Create(attempt).Error; err != nil {
fmt.Printf("记录登录尝试失败: %v\n", err)
}
}
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
func generateRandomToken(length int) string {
b := make([]byte, length)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
// GetUserByToken 通过令牌获取用户信息
func (s *AuthService) GetUserByToken(token string) (*models.User, error) {
claims, err := s.ValidateToken(token)
if err != nil {
return nil, err
}
var user models.User
if err := s.db.First(&user, claims.UserID).Error; err != nil {
return nil, err
}
return &user, nil
}
// GetUserSessions 获取用户会话列表
func (s *AuthService) GetUserSessions(userID uint) ([]models.Session, error) {
var sessions []models.Session
err := s.db.Where("user_id = ? AND is_active = true", userID).Find(&sessions).Error
return sessions, err
}
// RevokeSession 撤销会话
func (s *AuthService) RevokeSession(sessionID string, userID uint) error {
var session models.Session
if err := s.db.Where("session_id = ? AND user_id = ?", sessionID, userID).First(&session).Error; err != nil {
return errors.New("会话不存在")
}
session.IsActive = false
return s.db.Save(&session).Error
}