package auth import ( "errors" "sync" "time" "github.com/golang-jwt/jwt/v5" "github.com/wormhole/wormhole/internal/config" "github.com/wormhole/wormhole/internal/store" ) type Claims struct { UserID uint `json:"user_id"` Username string `json:"username"` Role string `json:"role"` jwt.RegisteredClaims } type Service struct { mu sync.RWMutex cfg *config.AuthConfig limiter *LoginLimiter } func NewService(cfg *config.AuthConfig) *Service { return &Service{ cfg: cfg, limiter: NewLoginLimiter(cfg), } } func (s *Service) ReloadAuth(cfg *config.AuthConfig) { s.mu.Lock() defer s.mu.Unlock() *s.cfg = *cfg s.limiter = NewLoginLimiter(cfg) } func (s *Service) Login(st *store.Store, ip, username, password string) (string, *store.User, error) { s.mu.RLock() limiter := s.limiter s.mu.RUnlock() if retry, blocked := limiter.IsBlocked(ip, username); blocked { return "", nil, &LoginBlockedError{RetryAfter: retry} } user, err := st.GetUserByUsername(username) if err != nil { limiter.RecordFailure(ip, username) time.Sleep(300 * time.Millisecond) return "", nil, errors.New("invalid credentials") } if !user.Status { limiter.RecordFailure(ip, username) time.Sleep(300 * time.Millisecond) return "", nil, errors.New("invalid credentials") } if !store.CheckPassword(user.PasswordHash, password) { limiter.RecordFailure(ip, username) time.Sleep(300 * time.Millisecond) return "", nil, errors.New("invalid credentials") } if user.Role == "visitor" { limiter.RecordFailure(ip, username) time.Sleep(300 * time.Millisecond) return "", nil, errors.New("访客账号无法登录管理后台") } limiter.Reset(ip, username) token, err := s.GenerateToken(user) if err != nil { return "", nil, err } return token, user, nil } func (s *Service) GenerateToken(user *store.User) (string, error) { s.mu.RLock() cfg := s.cfg s.mu.RUnlock() claims := Claims{ UserID: user.ID, Username: user.Username, Role: user.Role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(cfg.TokenTTL)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(cfg.JWTSecret)) } func (s *Service) ParseToken(tokenStr string) (*Claims, error) { s.mu.RLock() secret := s.cfg.JWTSecret s.mu.RUnlock() token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) { return []byte(secret), nil }) if err != nil { return nil, err } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, errors.New("invalid token") } return claims, nil } func IsAdmin(role string) bool { return role == "admin" } func IsVisitor(role string) bool { return role == "visitor" }