This repository has been archived on 2026-06-21. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MetaLab/internal/service/auth_service.go
Victor_Jay debaa17127 feat(settings): 用户名校验强化、个性签名编辑、字符计数器及提示优化
- 用户名校验改用白名单正则(中英文/数字/下划线/连字符),字符数计法统一
- 简介→个性签名,新增 textarea 可编辑,上限 128 字符
- 用户名+签名各增加实时字符计数器(x/16、x/128)
- API 合并为 PUT /api/settings/profile,一次提交两字段,按变更写入
- Toast 移至 NAV 下方,独立样式,5 秒自动消失
- 移除用户 ID 灰色底色,提示改为「个人资料已更新」
- 纯文本存储 + Go 模板自动转义防 XSS
2026-05-27 00:03:09 +08:00

240 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"regexp"
"unicode/utf8"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/model"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// AuthService 认证业务逻辑
type AuthService struct {
userRepo userAuthStore
tokenService tokenProvider
cfg *config.Config
}
// NewAuthService 构造函数
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, 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`)
// usernamePattern 用户名合法字符:中文、英文大小写、数字、下划线、连字符
var usernamePattern = regexp.MustCompile(`^[\p{Han}a-zA-Z0-9_-]+$`)
// 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)
}
// GetProfile 获取当前用户资料(供 SettingsController 使用)
func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
return s.userRepo.FindByID(userID)
}
// InvalidateSessions 吊销某用户所有 JWT递增 token_version强制所有设备重新登录
// 适用场景:修改密码、账号被盗、管理员强制下线
func (s *AuthService) InvalidateSessions(userID uint) error {
return s.userRepo.IncrementTokenVersion(userID)
}
// UpdateProfile 修改个人资料(用户名 + 个性签名)
// 用户名校验非空、1-16 字符、白名单、去重
// 个性签名校验0-128 字符纯文本Go 模板自动 HTML 转义防 XSS
// 任一字段无变更时跳过该字段的写库操作
func (s *AuthService) UpdateProfile(userID uint, username, bio string) error {
// 查当前用户
user, err := s.userRepo.FindByID(userID)
if err != nil {
return common.ErrUserNotFound
}
needsUpdate := false
// 用户名:校验 + 更新
if n := utf8.RuneCountInString(username); n == 0 {
return common.ErrUsernameInvalid
} else if n > 16 {
return common.ErrUsernameInvalid
}
if !usernamePattern.MatchString(username) {
return common.ErrUsernameInvalid
}
if username != user.Username {
exists, err := s.userRepo.ExistsByUsername(username)
if err != nil {
return err
}
if exists {
return common.ErrUsernameTaken
}
user.Username = username
needsUpdate = true
}
// 个性签名:长度校验 + 更新
if utf8.RuneCountInString(bio) > 128 {
return common.ErrBioTooLong
}
if bio != user.Bio {
user.Bio = bio
needsUpdate = true
}
if !needsUpdate {
return nil
}
return s.userRepo.Update(user)
}