初始化项目:基础设施 + 用户认证 + 后台管理系统 + AGPL 3.0 许可
This commit is contained in:
182
internal/service/auth_service.go
Normal file
182
internal/service/auth_service.go
Normal file
@ -0,0 +1,182 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/config"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/repository"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthService 认证业务逻辑
|
||||
type AuthService struct {
|
||||
userRepo *repository.UserRepo
|
||||
tokenService *TokenService
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewAuthService 构造函数
|
||||
func NewAuthService(userRepo *repository.UserRepo, tokenSvc *TokenService, cfg *config.Config) *AuthService {
|
||||
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrEmailExists = common.ErrEmailExists
|
||||
ErrUsernameTaken = common.ErrUsernameTaken
|
||||
ErrInvalidCred = common.ErrInvalidCred
|
||||
ErrUserNotFound = common.ErrUserNotFound
|
||||
ErrUserBanned = common.ErrUserBanned
|
||||
ErrUserLocked = common.ErrUserLocked
|
||||
ErrUserDeleted = common.ErrUserDeleted
|
||||
ErrWeakPassword = common.ErrWeakPassword
|
||||
ErrTokenExpired = common.ErrTokenExpired
|
||||
ErrTokenInvalid = common.ErrTokenInvalid
|
||||
ErrTokenRevoked = common.ErrTokenRevoked
|
||||
ErrPermissionDenied = common.ErrPermissionDenied
|
||||
)
|
||||
|
||||
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
|
||||
var pwDigit = regexp.MustCompile(`\d`)
|
||||
|
||||
// dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对
|
||||
// 在 init() 中按当前 bcrypt 成本生成,确保时序与真实校验一致
|
||||
var dummyHash []byte
|
||||
|
||||
func init() {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("metazone-dummy-hash-2026"), 12)
|
||||
if err != nil {
|
||||
panic("failed to generate bcrypt dummy hash: " + err.Error())
|
||||
}
|
||||
dummyHash = hash
|
||||
}
|
||||
|
||||
// validatePassword 密码强度:至少 8 位 + 包含字母 + 包含数字
|
||||
func validatePassword(pw string) error {
|
||||
if len(pw) < 8 || !pwLetter.MatchString(pw) || !pwDigit.MatchString(pw) {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register 注册
|
||||
// 流程:密码强度 → 邮箱查重 → 哈希 → 生成唯一用户名 → 创建 → access JWT + refresh JWT
|
||||
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
||||
func (s *AuthService) Register(req model.RegisterRequest) (string, string, *model.User, error) {
|
||||
// 1. 密码强度
|
||||
if err := validatePassword(req.Password); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 2. 检查邮箱
|
||||
exists, err := s.userRepo.ExistsByEmail(req.Email)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
if exists {
|
||||
return "", "", nil, ErrEmailExists
|
||||
}
|
||||
|
||||
// 3. 哈希密码
|
||||
hash, err := common.HashPassword(req.Password, s.cfg.Bcrypt.Cost)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 4. 生成唯一用户名
|
||||
username, err := common.GenerateUsername(s.userRepo, 20)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 5. 创建用户
|
||||
user := &model.User{
|
||||
Email: req.Email,
|
||||
PasswordHash: hash,
|
||||
Username: username,
|
||||
Role: model.RoleUser,
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 6. 生成 access JWT
|
||||
accessToken, err := s.tokenService.BuildAccessToken(user)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 7. 生成 refresh JWT(不勾选"记住我"也用 session cookie,关浏览器即清除)
|
||||
refreshToken, err := s.tokenService.BuildRefreshToken(user, req.RememberMe)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, user, nil
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
// 流程:按邮箱查找 → 检查状态 → 验证密码 → access JWT + refresh JWT
|
||||
// 防时序攻击:邮箱不存在时仍执行完整 bcrypt 比对
|
||||
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
||||
func (s *AuthService) Login(req model.LoginRequest) (string, string, *model.User, error) {
|
||||
user, err := s.userRepo.FindByEmail(req.Email)
|
||||
if err != nil {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, ErrInvalidCred
|
||||
}
|
||||
|
||||
// 已注销(deleted)→ 自动恢复
|
||||
if user.Status == model.StatusDeleted {
|
||||
user.Status = model.StatusActive
|
||||
user.DeletedAt = gorm.DeletedAt{}
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
// 恢复后继续正常登录流程
|
||||
}
|
||||
|
||||
// 永久锁定
|
||||
if user.Status == model.StatusLocked {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, ErrUserLocked
|
||||
}
|
||||
|
||||
// 封禁
|
||||
if user.Status == model.StatusBanned {
|
||||
return "", "", nil, ErrUserBanned
|
||||
}
|
||||
|
||||
if !common.CheckPassword(req.Password, user.PasswordHash) {
|
||||
return "", "", nil, ErrInvalidCred
|
||||
}
|
||||
|
||||
// 生成 access JWT
|
||||
accessToken, err := s.tokenService.BuildAccessToken(user)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// 生成 refresh JWT(不勾选"记住我"也生成,Cookie 用 session 模式)
|
||||
refreshToken, err := s.tokenService.BuildRefreshToken(user, req.RememberMe)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, user, nil
|
||||
}
|
||||
|
||||
// CheckEmail 检查邮箱是否已被注册(无需认证的轻量检查)
|
||||
func (s *AuthService) CheckEmail(email string) (bool, error) {
|
||||
return s.userRepo.ExistsByEmail(email)
|
||||
}
|
||||
|
||||
// InvalidateSessions 吊销某用户所有 JWT(递增 token_version,强制所有设备重新登录)
|
||||
// 适用场景:修改密码、账号被盗、管理员强制下线
|
||||
func (s *AuthService) InvalidateSessions(userID uint) error {
|
||||
return s.userRepo.IncrementTokenVersion(userID)
|
||||
}
|
||||
Reference in New Issue
Block a user