Files
filestoragesystem/go/internal/service/auth.go
T
2026-08-23 00:48:10 +08:00

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
}