package service import ( "context" "crypto/rand" "encoding/base64" "fmt" mathrand "math/rand" "net/http" "strings" "time" "gofaster/internal/auth/model" "gofaster/internal/auth/repository" "gofaster/internal/shared/jwt" "golang.org/x/crypto/bcrypt" ) type AuthService interface { Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) Logout(ctx context.Context, token string) error RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) } type authService struct { userRepo repository.UserRepository captchaRepo repository.CaptchaRepository jwtManager jwt.JWTManager } func NewAuthService(userRepo repository.UserRepository, captchaRepo repository.CaptchaRepository, jwtManager jwt.JWTManager) AuthService { return &authService{ userRepo: userRepo, captchaRepo: captchaRepo, jwtManager: jwtManager, } } // Login 用户登录 func (s *authService) Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) { // 1. 验证验证码 if err := s.ValidateCaptcha(ctx, req.Username, req.Captcha); err != nil { return nil, fmt.Errorf("验证码错误: %w", err) } // 2. 根据用户名查找用户 user, err := s.userRepo.GetByUsername(ctx, req.Username) if err != nil { return nil, fmt.Errorf("用户不存在") } // 3. 检查用户状态 if !user.CanLogin() { if user.IsLocked() { return nil, fmt.Errorf("账户已被锁定,请30分钟后再试") } return nil, fmt.Errorf("账户已被禁用") } // 4. 验证密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { // 密码错误,增加错误次数 if err := s.userRepo.IncrementPasswordError(ctx, user.ID); err != nil { return nil, fmt.Errorf("系统错误,请稍后重试") } // 检查是否被锁定 if user.PasswordErrorCount >= 4 { // 已经是第5次错误 return nil, fmt.Errorf("密码错误次数过多,账户已被锁定30分钟") } remaining := 5 - user.PasswordErrorCount - 1 return nil, fmt.Errorf("密码错误,还可尝试%d次", remaining) } // 5. 密码正确,重置错误次数并更新登录信息 if err := s.userRepo.ResetPasswordError(ctx, user.ID); err != nil { return nil, fmt.Errorf("系统错误,请稍后重试") } if err := s.userRepo.UpdateLastLogin(ctx, user.ID, clientIP); err != nil { // 登录信息更新失败不影响登录流程 fmt.Printf("更新登录信息失败: %v\n", err) } // 6. 生成JWT令牌 claims := map[string]interface{}{ "user_id": user.ID, "username": user.Username, "email": user.Email, } token, err := s.jwtManager.GenerateToken(claims, 24*time.Hour) // 24小时有效期 if err != nil { return nil, fmt.Errorf("生成令牌失败: %w", err) } refreshToken, err := s.jwtManager.GenerateToken(claims, 7*24*time.Hour) // 7天有效期 if err != nil { return nil, fmt.Errorf("生成刷新令牌失败: %w", err) } // 7. 获取用户角色信息 userWithRoles, err := s.userRepo.GetUserWithRoles(ctx, user.ID) if err != nil { return nil, fmt.Errorf("获取用户信息失败: %w", err) } // 8. 构建响应 userInfo := s.buildUserInfo(userWithRoles) return &model.LoginResponse{ Token: token, TokenType: "Bearer", ExpiresIn: 24 * 60 * 60, // 24小时,单位秒 RefreshToken: refreshToken, User: *userInfo, }, nil } // Logout 用户登出 func (s *authService) Logout(ctx context.Context, token string) error { // 这里可以实现令牌黑名单机制 // 目前简单返回成功 return nil } // RefreshToken 刷新令牌 func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) { // 验证刷新令牌 claims, err := s.jwtManager.ValidateToken(refreshToken) if err != nil { return nil, fmt.Errorf("刷新令牌无效: %w", err) } // 获取用户信息 userID, ok := claims["user_id"].(float64) if !ok { return nil, fmt.Errorf("令牌格式错误") } user, err := s.userRepo.GetUserWithRoles(ctx, uint(userID)) if err != nil { return nil, fmt.Errorf("用户不存在: %w", err) } // 检查用户状态 if !user.CanLogin() { return nil, fmt.Errorf("用户状态异常") } // 生成新的访问令牌 newClaims := map[string]interface{}{ "user_id": user.ID, "username": user.Username, "email": user.Email, } newToken, err := s.jwtManager.GenerateToken(newClaims, 24*time.Hour) if err != nil { return nil, fmt.Errorf("生成新令牌失败: %w", err) } // 构建响应 userInfo := s.buildUserInfo(user) return &model.LoginResponse{ Token: newToken, TokenType: "Bearer", ExpiresIn: 24 * 60 * 60, RefreshToken: refreshToken, // 保持原刷新令牌 User: *userInfo, }, nil } // GenerateCaptcha 生成验证码 func (s *authService) GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) { // 生成随机验证码ID captchaID, err := s.generateRandomID() if err != nil { return nil, fmt.Errorf("生成验证码ID失败: %w", err) } // 生成4位随机验证码 captchaText := s.generateRandomCaptcha(4) // 设置5分钟过期时间 expiresAt := time.Now().Add(5 * time.Minute) // 保存到数据库 if err := s.captchaRepo.Create(ctx, captchaID, captchaText, expiresAt); err != nil { return nil, fmt.Errorf("保存验证码失败: %w", err) } // 生成验证码图片(这里简化处理,实际应该生成图片) captchaImage := s.generateCaptchaImage(captchaText) return &model.CaptchaResponse{ CaptchaID: captchaID, CaptchaImage: captchaImage, ExpiresIn: 5 * 60, // 5分钟,单位秒 }, nil } // ValidateCaptcha 验证验证码 func (s *authService) ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error { // 从数据库获取验证码 storedText, err := s.captchaRepo.Get(ctx, captchaID) if err != nil { return fmt.Errorf("验证码已过期或不存在") } // 比较验证码(不区分大小写) if !strings.EqualFold(storedText, captchaText) { return fmt.Errorf("验证码错误") } return nil } // GetUserInfo 获取用户信息 func (s *authService) GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) { user, err := s.userRepo.GetUserWithRoles(ctx, userID) if err != nil { return nil, fmt.Errorf("用户不存在: %w", err) } return s.buildUserInfo(user), nil } // buildUserInfo 构建用户信息 func (s *authService) buildUserInfo(user *model.User) *model.UserInfo { roles := make([]model.RoleInfo, 0, len(user.Roles)) for _, role := range user.Roles { roles = append(roles, model.RoleInfo{ ID: role.ID, Name: role.Name, Code: role.Code, }) } return &model.UserInfo{ ID: user.ID, Username: user.Username, Email: user.Email, Phone: user.Phone, Status: user.Status, LastLoginAt: user.LastLoginAt, LastLoginIP: user.LastLoginIP, Roles: roles, } } // generateRandomID 生成随机ID func (s *authService) generateRandomID() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } // generateRandomCaptcha 生成随机验证码 func (s *authService) generateRandomCaptcha(length int) string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" result := make([]byte, length) for i := range result { result[i] = chars[mathrand.Intn(len(chars))] } return string(result) } // generateCaptchaImage 生成验证码图片(简化版本,返回Base64编码) func (s *authService) generateCaptchaImage(text string) string { // 这里应该使用图片生成库生成实际的验证码图片 // 目前返回一个占位符 return fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(text))) } // GetClientIP 获取客户端IP地址 func GetClientIP(r *http.Request) string { // 尝试从各种头部获取真实IP ip := r.Header.Get("X-Real-IP") if ip != "" { return ip } ip = r.Header.Get("X-Forwarded-For") if ip != "" { // X-Forwarded-For可能包含多个IP,取第一个 if idx := strings.Index(ip, ","); idx != -1 { ip = ip[:idx] } return strings.TrimSpace(ip) } ip = r.Header.Get("X-Forwarded") if ip != "" { return ip } // 从RemoteAddr获取 if r.RemoteAddr != "" { if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 { return r.RemoteAddr[:idx] } return r.RemoteAddr } return "127.0.0.1" }