347 lines
8.8 KiB
Go
347 lines
8.8 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"filestoragesystem/internal/config"
|
|
"filestoragesystem/internal/model"
|
|
"filestoragesystem/internal/repository"
|
|
"filestoragesystem/internal/utils"
|
|
"filestoragesystem/pkg/apperr"
|
|
)
|
|
|
|
const (
|
|
maxLoginFails = 5 // 最大登录失败次数
|
|
lockDuration = 15 * time.Minute // 锁定时长
|
|
signTimeWindow = 5 * time.Minute // 签名时间戳有效窗口
|
|
)
|
|
|
|
// AuthService 认证服务
|
|
type AuthService struct {
|
|
cfg *config.Config
|
|
userRepo *repository.UserRepo
|
|
apiKeyRepo *repository.APIKeyRepo
|
|
roleRepo *repository.RoleRepo
|
|
settingRepo *repository.SettingRepo
|
|
opLogRepo *repository.OpLogRepo
|
|
|
|
mu sync.Mutex
|
|
loginFails map[string]*loginState // 登录失败状态
|
|
nonces map[string]time.Time // 防重放nonce
|
|
}
|
|
|
|
type loginState struct {
|
|
fails int
|
|
lockedUntil time.Time
|
|
}
|
|
|
|
// ensureState 惰性初始化登录失败与nonce缓存
|
|
func (s *AuthService) ensureState() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.loginFails == nil {
|
|
s.loginFails = make(map[string]*loginState)
|
|
}
|
|
if s.nonces == nil {
|
|
s.nonces = make(map[string]time.Time)
|
|
}
|
|
}
|
|
|
|
// Register 用户注册
|
|
func (s *AuthService) Register(username, email, password string) (*model.User, error) {
|
|
// 注册开关
|
|
if v, err := s.settingRepo.GetValue("register_enabled"); err == nil && v != "true" {
|
|
return nil, apperr.ErrRegisterClosed
|
|
}
|
|
username = strings.TrimSpace(username)
|
|
email = strings.TrimSpace(strings.ToLower(email))
|
|
|
|
if len(username) < 3 || len(username) > 50 {
|
|
return nil, fmt.Errorf("用户名长度需在3-50之间")
|
|
}
|
|
for _, ch := range username {
|
|
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '-' && ch < 0x80 {
|
|
return nil, fmt.Errorf("用户名仅支持字母、数字、下划线和横线")
|
|
}
|
|
}
|
|
if !utils.IsEmail(email) {
|
|
return nil, fmt.Errorf("邮箱格式不正确")
|
|
}
|
|
if !utils.IsValidPassword(password) {
|
|
return nil, fmt.Errorf("密码至少8位且需包含字母和数字")
|
|
}
|
|
if _, err := s.userRepo.FindByUsername(username); err == nil {
|
|
return nil, apperr.ErrUserExists
|
|
}
|
|
if _, err := s.userRepo.FindByEmail(email); err == nil {
|
|
return nil, apperr.ErrUserExists
|
|
}
|
|
|
|
// 默认角色与配额
|
|
role, err := s.roleRepo.FindByCode("user")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("默认角色不存在: %w", err)
|
|
}
|
|
limit := int64(10737418240)
|
|
if v, err := s.settingRepo.GetValue("default_storage_limit"); err == nil {
|
|
var n int64
|
|
if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 {
|
|
limit = n
|
|
}
|
|
}
|
|
|
|
hash, err := utils.HashPassword(password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u := &model.User{
|
|
Username: username, Email: email, PasswordHash: hash,
|
|
RoleID: role.ID, Status: 1, StorageLimit: limit,
|
|
}
|
|
if err := s.userRepo.Create(u); err != nil {
|
|
return nil, err
|
|
}
|
|
utils.Info("auth", "新用户注册: %s", username)
|
|
return u, nil
|
|
}
|
|
|
|
// Login 用户登录,成功返回用户与JWT token
|
|
func (s *AuthService) Login(account, password, ip string) (*model.User, string, error) {
|
|
s.ensureState()
|
|
account = strings.TrimSpace(account)
|
|
|
|
// 锁定检查
|
|
s.mu.Lock()
|
|
if st, ok := s.loginFails[account]; ok {
|
|
if time.Now().Before(st.lockedUntil) {
|
|
s.mu.Unlock()
|
|
return nil, "", apperr.ErrAccountLocked
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
u, err := s.userRepo.FindByUsernameOrEmail(account)
|
|
if err != nil {
|
|
s.recordLoginFail(account)
|
|
return nil, "", apperr.ErrPasswordWrong
|
|
}
|
|
if u.Status != 1 {
|
|
return nil, "", apperr.ErrUserDisabled
|
|
}
|
|
if !utils.CheckPassword(u.PasswordHash, password) {
|
|
s.recordLoginFail(account)
|
|
return nil, "", apperr.ErrPasswordWrong
|
|
}
|
|
|
|
// 登录成功:清除失败计数
|
|
s.mu.Lock()
|
|
delete(s.loginFails, account)
|
|
s.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
_ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{
|
|
"last_login_at": now, "last_login_ip": ip,
|
|
})
|
|
|
|
token, err := utils.GenerateToken(u.ID, u.Username, roleCodeOf(u))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return u, token, nil
|
|
}
|
|
|
|
func (s *AuthService) recordLoginFail(account string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.loginFails == nil {
|
|
s.loginFails = make(map[string]*loginState)
|
|
}
|
|
st, ok := s.loginFails[account]
|
|
if !ok {
|
|
st = &loginState{}
|
|
s.loginFails[account] = st
|
|
}
|
|
st.fails++
|
|
if st.fails >= maxLoginFails {
|
|
st.lockedUntil = time.Now().Add(lockDuration)
|
|
st.fails = 0
|
|
utils.Warn("auth", "账户 %s 因连续登录失败被锁定%d分钟", account, int(lockDuration.Minutes()))
|
|
}
|
|
}
|
|
|
|
// roleCodeOf 用户角色编码(内存缓存于User.Role)
|
|
func roleCodeOf(u *model.User) string {
|
|
if u.Role != nil {
|
|
return u.Role.Code
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// CreateAPIKey 为用户生成API密钥,secret仅此一次返回
|
|
func (s *AuthService) CreateAPIKey(userID uint, name string) (*model.APIKey, string, error) {
|
|
u, err := s.userRepo.FindByID(userID)
|
|
if err != nil {
|
|
return nil, "", apperr.ErrNotFound
|
|
}
|
|
key := &model.APIKey{
|
|
UserID: userID,
|
|
AccessKey: utils.RandomKey(32),
|
|
SecretKey: utils.RandomKey(64),
|
|
Name: name,
|
|
Status: 1,
|
|
}
|
|
if key.Name == "" {
|
|
key.Name = u.Username + "的密钥"
|
|
}
|
|
if err := s.apiKeyRepo.Create(key); err != nil {
|
|
return nil, "", err
|
|
}
|
|
utils.Info("auth", "用户 %s 创建API密钥: %s", u.Username, key.AccessKey)
|
|
return key, key.SecretKey, nil
|
|
}
|
|
|
|
// ListAPIKeys 用户密钥列表
|
|
func (s *AuthService) ListAPIKeys(userID uint) ([]model.APIKey, error) {
|
|
return s.apiKeyRepo.ListByUser(userID)
|
|
}
|
|
|
|
// DeleteAPIKey 删除密钥
|
|
func (s *AuthService) DeleteAPIKey(userID, id uint) error {
|
|
if err := s.apiKeyRepo.Delete(id, userID); err != nil {
|
|
return apperr.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyAPIKeySignature 校验API Key HMAC-SHA256签名,返回所属用户
|
|
// 签名串: accessKey + "\n" + timestamp + "\n" + nonce + "\n" + method + "\n" + path
|
|
func (s *AuthService) VerifyAPIKeySignature(accessKey, timestamp, nonce, signature, method, path string) (*model.User, error) {
|
|
s.ensureState()
|
|
|
|
// 时间窗口
|
|
var ts int64
|
|
if _, err := fmt.Sscanf(timestamp, "%d", &ts); err != nil {
|
|
return nil, apperr.ErrSignInvalid
|
|
}
|
|
t := time.Unix(ts, 0)
|
|
if diff := time.Since(t); diff > signTimeWindow || diff < -signTimeWindow {
|
|
return nil, apperr.ErrSignExpired
|
|
}
|
|
|
|
// 防重放
|
|
s.mu.Lock()
|
|
if s.nonces == nil {
|
|
s.nonces = make(map[string]time.Time)
|
|
}
|
|
if _, used := s.nonces[nonce]; used {
|
|
s.mu.Unlock()
|
|
return nil, apperr.ErrSignInvalid
|
|
}
|
|
s.nonces[nonce] = time.Now()
|
|
// 顺带清理过期nonce
|
|
if len(s.nonces) > 10000 {
|
|
cutoff := time.Now().Add(-signTimeWindow * 2)
|
|
for k, v := range s.nonces {
|
|
if v.Before(cutoff) {
|
|
delete(s.nonces, k)
|
|
}
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
key, err := s.apiKeyRepo.FindByAccessKey(accessKey)
|
|
if err != nil {
|
|
return nil, apperr.ErrSignInvalid
|
|
}
|
|
if key.Status != 1 {
|
|
return nil, apperr.ErrForbidden
|
|
}
|
|
|
|
message := strings.Join([]string{accessKey, timestamp, nonce, method, path}, "\n")
|
|
expect := utils.HMACSHA256(key.SecretKey, message)
|
|
if !strings.EqualFold(expect, signature) {
|
|
return nil, apperr.ErrSignInvalid
|
|
}
|
|
|
|
_ = s.apiKeyRepo.Touch(key.ID)
|
|
|
|
u, err := s.userRepo.FindByID(key.UserID)
|
|
if err != nil {
|
|
return nil, apperr.ErrNotFound
|
|
}
|
|
if u.Status != 1 {
|
|
return nil, apperr.ErrUserDisabled
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// LoadUserByID 加载用户(含角色权限)
|
|
func (s *AuthService) LoadUserByID(id uint) (*model.User, error) {
|
|
u, err := s.userRepo.FindByID(id)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, apperr.ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
if u.Status != 1 {
|
|
return nil, apperr.ErrUserDisabled
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// PermissionCodesOf 用户拥有的权限编码集合
|
|
func PermissionCodesOf(u *model.User) map[string]bool {
|
|
codes := make(map[string]bool)
|
|
if u == nil || u.Role == nil {
|
|
return codes
|
|
}
|
|
if u.Role.Code == "super_admin" {
|
|
codes["*"] = true
|
|
return codes
|
|
}
|
|
for _, p := range u.Role.Permissions {
|
|
codes[p.Code] = true
|
|
}
|
|
return codes
|
|
}
|
|
|
|
// BootstrapAdmin 按配置确保管理员账户存在
|
|
func (s *AuthService) BootstrapAdmin() error {
|
|
ac := s.cfg.Admin
|
|
if ac.Username == "" {
|
|
return nil
|
|
}
|
|
u, err := s.userRepo.FindByUsername(ac.Username)
|
|
if err == nil {
|
|
// 已存在:确保其为配置的角色
|
|
if role, rerr := s.roleRepo.FindByCode(ac.Role); rerr == nil && u.RoleID != role.ID {
|
|
_ = s.userRepo.UpdateFields(u.ID, map[string]interface{}{"role_id": role.ID, "status": 1})
|
|
}
|
|
return nil
|
|
}
|
|
role, err := s.roleRepo.FindByCode(ac.Role)
|
|
if err != nil {
|
|
return fmt.Errorf("配置的管理员角色 %s 不存在: %w", ac.Role, err)
|
|
}
|
|
hash, err := utils.HashPassword(ac.Password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
admin := &model.User{
|
|
Username: ac.Username, Email: ac.Email, PasswordHash: hash,
|
|
RoleID: role.ID, Status: 1, StorageLimit: 1 << 40,
|
|
}
|
|
if err := s.userRepo.Create(admin); err != nil {
|
|
return err
|
|
}
|
|
utils.Info("auth", "已创建管理员账户: %s", ac.Username)
|
|
return nil
|
|
}
|