package auth import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" "golang.org/x/crypto/bcrypt" ) // User 表示系统用户 type User struct { Username string `json:"username"` Password string `json:"-"` // bcrypt hash,不序列化到前端 Hash string `json:"hash,omitempty"` // 持久化用 Role string `json:"role"` CreatedAt int64 `json:"created_at"` } // UserStore 负责用户持久化与校验,内存为主,可选落盘到 JSON 文件 type UserStore struct { mu sync.RWMutex users map[string]*User filePath string } // NewUserStore 创建用户存储,若 filePath 非空则尝试从文件加载;若文件不存在则预置种子用户 func NewUserStore(filePath string) (*UserStore, error) { s := &UserStore{ users: make(map[string]*User), filePath: filePath, } if filePath != "" { if err := s.load(); err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("load users: %w", err) } s.seed() _ = s.save() } else if len(s.users) == 0 { s.seed() _ = s.save() } } else { s.seed() } return s, nil } func (s *UserStore) seed() { seeds := []struct { username string password string role string }{ {"admin", "Admin123!", RoleAdmin}, {"publisher", "Publisher123!", RolePublisher}, {"viewer", "Viewer123!", RoleViewer}, } for _, u := range seeds { hash, _ := bcrypt.GenerateFromPassword([]byte(u.password), bcrypt.DefaultCost) s.users[u.username] = &User{ Username: u.username, Password: string(hash), Hash: string(hash), Role: u.role, CreatedAt: time.Now().Unix(), } } } func (s *UserStore) Get(username string) (*User, bool) { s.mu.RLock() defer s.mu.RUnlock() u, ok := s.users[username] if !ok { return nil, false } cp := *u return &cp, true } func (s *UserStore) List() []*User { s.mu.RLock() defer s.mu.RUnlock() out := make([]*User, 0, len(s.users)) for _, u := range s.users { out = append(out, &User{Username: u.Username, Role: u.Role, CreatedAt: u.CreatedAt}) } return out } func (s *UserStore) Create(username, password, role string) (*User, error) { if username == "" || password == "" { return nil, fmt.Errorf("username and password required") } if role == "" { role = RoleViewer } if !isValidRole(role) { return nil, fmt.Errorf("invalid role %q", role) } if len(password) < 6 { return nil, fmt.Errorf("password too short (min 6)") } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.users[username]; exists { return nil, fmt.Errorf("user %q already exists", username) } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return nil, err } u := &User{ Username: username, Password: string(hash), Hash: string(hash), Role: role, CreatedAt: time.Now().Unix(), } s.users[username] = u _ = s.saveLocked() cp := *u cp.Password = "" cp.Hash = "" return &cp, nil } func (s *UserStore) Verify(username, password string) (*User, bool) { s.mu.RLock() u, ok := s.users[username] s.mu.RUnlock() if !ok { return nil, false } if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil { return nil, false } cp := *u return &cp, true } func (s *UserStore) UpdateRole(username, role string) error { if !isValidRole(role) { return fmt.Errorf("invalid role %q", role) } s.mu.Lock() defer s.mu.Unlock() u, ok := s.users[username] if !ok { return fmt.Errorf("user %q not found", username) } u.Role = role return s.saveLocked() } func (s *UserStore) load() error { data, err := os.ReadFile(s.filePath) if err != nil { return err } var list []*User if err := json.Unmarshal(data, &list); err != nil { return err } for _, u := range list { if u.Password == "" && u.Hash != "" { u.Password = u.Hash } if u.Hash == "" && u.Password != "" { u.Hash = u.Password } s.users[u.Username] = u } return nil } func (s *UserStore) save() error { s.mu.RLock() defer s.mu.RUnlock() return s.saveLocked() } func (s *UserStore) saveLocked() error { if s.filePath == "" { return nil } if err := os.MkdirAll(filepath.Dir(s.filePath), 0755); err != nil { return err } list := make([]*User, 0, len(s.users)) for _, u := range s.users { if u.Hash == "" { u.Hash = u.Password } list = append(list, u) } data, _ := json.MarshalIndent(list, "", " ") return os.WriteFile(s.filePath, data, 0644) } const ( RoleAdmin = "admin" RolePublisher = "publisher" RoleViewer = "viewer" RoleGuest = "guest" ) func isValidRole(r string) bool { switch r { case RoleAdmin, RolePublisher, RoleViewer: return true default: return false } } func AllRoles() []string { return []string{RoleAdmin, RolePublisher, RoleViewer} }