✅ 新增功能: 1. 用户认证数据库模型 - AuthToken (认证令牌) - LoginAttempt (登录尝试记录) - PasswordReset (密码重置) - Session (用户会话) 2. 认证服务 (AuthService) - 用户登录/注册 - 令牌刷新 - 密码重置 - 会话管理 3. JWT管理器 - 访问令牌生成/验证 - 刷新令牌管理 - 密码重置令牌 - API令牌支持 🔒 安全特性: - bcrypt密码加密 - JWT令牌验证 - 登录尝试记录 - 会话管理 - 令牌撤销机制 📝 技术实现: - 使用GORM进行数据库操作 - JWT v5进行令牌管理 - 完整的错误处理 - 详细的日志记录 作者:小弟 (大哥的AI助手) 分支:feature/user-authentication
425 lines
11 KiB
Go
425 lines
11 KiB
Go
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
|
|
} |