package users import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/json" "errors" "fmt" "net/mail" "os" "strings" "sync" "time" ) type Role string const ( RoleUser Role = "user" RoleDeveloper Role = "developer" RoleAdmin Role = "admin" ) type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Role Role `json:"role"` Active bool `json:"active"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` PasswordHash string `json:"-"` } type persistedUser struct { User PasswordHash string `json:"password_hash"` } type database struct { Users []persistedUser `json:"users"` } type session struct { UserID string ExpiresAt time.Time } type Store struct { path string mu sync.RWMutex users map[string]persistedUser sessions map[string]session } var ( emailPattern = mail.Address{} allowedRoles = map[Role]bool{RoleUser: true, RoleDeveloper: true, RoleAdmin: true} passwordIterations = 150000 ) func New(path string) (*Store, error) { if strings.TrimSpace(path) == "" { return nil, errors.New("用户数据文件不能为空") } store := &Store{path: path, users: make(map[string]persistedUser), sessions: make(map[string]session)} if err := store.load(); err != nil { return nil, err } return store, nil } func ValidateEmail(value string) error { value = strings.TrimSpace(value) if value == "" || len(value) > 254 { return errors.New("邮箱地址无效") } address, err := mail.ParseAddress(value) if err != nil || address.Address != value || !strings.Contains(value, "@") { return errors.New("邮箱地址无效") } return nil } func ValidatePassword(value string) error { if len(value) < 8 || len(value) > 128 { return errors.New("密码长度必须为 8 到 128 个字符") } return nil } func (s *Store) Register(email, name, password string) (User, error) { email = strings.ToLower(strings.TrimSpace(email)) name = strings.TrimSpace(name) if err := ValidateEmail(email); err != nil { return User{}, err } if err := ValidatePassword(password); err != nil { return User{}, err } if len(name) > 80 { return User{}, errors.New("用户名长度不能超过 80 个字符") } if name == "" { name = strings.Split(email, "@")[0] } salt, err := randomBytes(16) if err != nil { return User{}, fmt.Errorf("生成密码盐失败: %w", err) } stored := persistedUser{User: User{ID: newID(), Email: email, Name: name, Role: RoleUser, Active: true, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, PasswordHash: encodePassword(password, salt)} s.mu.Lock() defer s.mu.Unlock() for _, existing := range s.users { if existing.Email == email { return User{}, errors.New("该邮箱已经注册") } } s.users[stored.ID] = stored if err := s.saveLocked(); err != nil { delete(s.users, stored.ID) return User{}, err } return stored.User, nil } func (s *Store) EnsureAdmin(email, name, password string) (User, error) { email = strings.ToLower(strings.TrimSpace(email)) if email == "" && password == "" { return User{}, nil } if email == "" || password == "" { return User{}, errors.New("管理员邮箱和密码必须同时配置") } if err := ValidateEmail(email); err != nil { return User{}, err } if err := ValidatePassword(password); err != nil { return User{}, err } s.mu.Lock() defer s.mu.Unlock() for id, existing := range s.users { if existing.Email == email { if existing.User.Role != RoleAdmin || !existing.User.Active { existing.User.Role = RoleAdmin existing.User.Active = true existing.User.UpdatedAt = time.Now().UTC() s.users[id] = existing if err := s.saveLocked(); err != nil { return User{}, err } } return existing.User, nil } } salt, err := randomBytes(16) if err != nil { return User{}, fmt.Errorf("生成管理员密码盐失败: %w", err) } stored := persistedUser{User: User{ID: newID(), Email: email, Name: strings.TrimSpace(name), Role: RoleAdmin, Active: true, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, PasswordHash: encodePassword(password, salt)} if stored.Name == "" { stored.Name = "管理员" } s.users[stored.ID] = stored if err := s.saveLocked(); err != nil { delete(s.users, stored.ID) return User{}, err } return stored.User, nil } func (s *Store) Authenticate(email, password string) (User, error) { email = strings.ToLower(strings.TrimSpace(email)) s.mu.RLock() defer s.mu.RUnlock() for _, candidate := range s.users { if candidate.Email != email { continue } if !candidate.User.Active || !verifyPassword(password, candidate.PasswordHash) { return User{}, errors.New("邮箱或密码错误") } return candidate.User, nil } return User{}, errors.New("邮箱或密码错误") } func (s *Store) CreateSession(userID string, duration time.Duration) (string, time.Time, error) { if duration <= 0 { duration = 24 * time.Hour } if _, ok := s.Get(userID); !ok { return "", time.Time{}, errors.New("用户不存在") } tokenBytes, err := randomBytes(32) if err != nil { return "", time.Time{}, fmt.Errorf("生成会话令牌失败: %w", err) } token := base64.RawURLEncoding.EncodeToString(tokenBytes) expiresAt := time.Now().UTC().Add(duration) s.mu.Lock() s.sessions[token] = session{UserID: userID, ExpiresAt: expiresAt} s.mu.Unlock() return token, expiresAt, nil } func (s *Store) UserBySession(token string) (User, bool) { token = strings.TrimSpace(token) if token == "" { return User{}, false } s.mu.Lock() defer s.mu.Unlock() current, ok := s.sessions[token] if !ok { return User{}, false } if time.Now().UTC().After(current.ExpiresAt) { delete(s.sessions, token) return User{}, false } stored, ok := s.users[current.UserID] if !ok || !stored.User.Active { return User{}, false } return stored.User, true } func (s *Store) RevokeSession(token string) { s.mu.Lock() delete(s.sessions, strings.TrimSpace(token)) s.mu.Unlock() } func (s *Store) Get(id string) (User, bool) { s.mu.RLock() defer s.mu.RUnlock() stored, ok := s.users[id] return stored.User, ok } func (s *Store) List() []User { s.mu.RLock() defer s.mu.RUnlock() result := make([]User, 0, len(s.users)) for _, stored := range s.users { result = append(result, stored.User) } return result } func (s *Store) ListByRole(role Role) []User { users := s.List() result := make([]User, 0) for _, user := range users { if user.Role == role && user.Active { result = append(result, user) } } return result } func (s *Store) Update(id string, role Role, active bool) (User, error) { if !allowedRoles[role] { return User{}, errors.New("用户角色无效") } s.mu.Lock() defer s.mu.Unlock() stored, ok := s.users[id] if !ok { return User{}, errors.New("用户不存在") } stored.User.Role = role stored.User.Active = active stored.User.UpdatedAt = time.Now().UTC() s.users[id] = stored if err := s.saveLocked(); err != nil { return User{}, err } return stored.User, nil } func (s *Store) load() error { data, err := os.ReadFile(s.path) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("读取用户数据失败: %w", err) } var stored database if err := json.Unmarshal(data, &stored); err != nil { return fmt.Errorf("解析用户数据失败: %w", err) } for _, user := range stored.Users { if user.User.ID != "" && user.User.Email != "" { s.users[user.User.ID] = user } } return nil } func (s *Store) saveLocked() error { if err := os.MkdirAll(filepathDir(s.path), 0o755); err != nil { return fmt.Errorf("创建用户数据目录失败: %w", err) } stored := database{Users: make([]persistedUser, 0, len(s.users))} for _, user := range s.users { stored.Users = append(stored.Users, user) } data, err := json.MarshalIndent(stored, "", " ") if err != nil { return fmt.Errorf("编码用户数据失败: %w", err) } temp, err := os.CreateTemp(filepathDir(s.path), ".users-*") if err != nil { return fmt.Errorf("创建用户临时文件失败: %w", err) } tempName := temp.Name() defer os.Remove(tempName) if _, err := temp.Write(data); err != nil { temp.Close() return fmt.Errorf("写入用户数据失败: %w", err) } if err := temp.Close(); err != nil { return fmt.Errorf("关闭用户临时文件失败: %w", err) } if err := os.Rename(tempName, s.path); err != nil { return fmt.Errorf("保存用户数据失败: %w", err) } return nil } func encodePassword(password string, salt []byte) string { key := derive(password, salt) return fmt.Sprintf("$scout$1$%d$%s$%s", passwordIterations, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key)) } func verifyPassword(password, encoded string) bool { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[1] != "scout" || parts[2] != "1" { return false } var iterations int if _, err := fmt.Sscanf(parts[3], "%d", &iterations); err != nil || iterations < 100000 { return false } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false } expected, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false } actual := deriveWithIterations(password, salt, iterations) return subtle.ConstantTimeCompare(actual, expected) == 1 } func derive(password string, salt []byte) []byte { return deriveWithIterations(password, salt, passwordIterations) } func deriveWithIterations(password string, salt []byte, iterations int) []byte { result := make([]byte, 32) mac := hmac.New(sha256.New, []byte(password)) mac.Write(salt) mac.Write([]byte{0, 0, 0, 1}) previous := mac.Sum(nil) copy(result, previous) for index := 1; index < iterations; index++ { mac = hmac.New(sha256.New, []byte(password)) mac.Write(previous) previous = mac.Sum(nil) for offset := range result { result[offset] ^= previous[offset] } } return result } func randomBytes(size int) ([]byte, error) { value := make([]byte, size) _, err := rand.Read(value) return value, err } func newID() string { value, err := randomBytes(16) if err != nil { return fmt.Sprintf("user-%d", time.Now().UnixNano()) } return base64.RawURLEncoding.EncodeToString(value) } func filepathDir(path string) string { index := strings.LastIndexAny(path, `/\\`) if index < 0 { return "." } return path[:index] } func sanitizeUser(user User) User { user.PasswordHash = "" return user } func (s *Store) SetPassword(id, password string) error { if err := ValidatePassword(password); err != nil { return err } salt, err := randomBytes(16) if err != nil { return fmt.Errorf("生成密码盐失败: %w", err) } s.mu.Lock() defer s.mu.Unlock() stored, ok := s.users[id] if !ok { return errors.New("用户不存在") } stored.PasswordHash = encodePassword(password, salt) stored.User.UpdatedAt = time.Now().UTC() s.users[id] = stored if err := s.saveLocked(); err != nil { return err } for token, current := range s.sessions { if current.UserID == id { delete(s.sessions, token) } } return nil } func (s *Store) FindByEmail(email string) (User, bool) { email = strings.ToLower(strings.TrimSpace(email)) s.mu.RLock() defer s.mu.RUnlock() for _, stored := range s.users { if stored.Email == email { return stored.User, true } } return User{}, false }