更新前后端代码
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user