452 lines
11 KiB
Go
452 lines
11 KiB
Go
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
|
|
}
|