refactor: JWT 无状态认证替换为服务端 Session,修复记住我掉线问题
- 新增 internal/session 包:Session 结构体、Store 接口、MemoryStore(内存+后台清理)、RedisStore 预留 - Cookie 从 3 个精简为 1 个 mlb_sid(HttpOnly),移除 JWT access/refresh cookie - 会话过期采用滑动窗口:每次请求自动续期,记住我 30 天无操作过期 - AuthMiddleware/AuthAdmin/Maintenance 中间件改用 SessionManager.Validate() - AuthService Login/Register/ConfirmRestore 去除 token 生成,返回 *model.User - Controller 层在登录/注册后调用 SessionManager.Create 创建会话 - AdminService UpdateUserStatus/ResetUserToken 同步销毁 session 实现即时退登 - 改密/注销时通过 DestroyByUID 删除所有 session(替换 TokenVersion 检查) - config.yaml 新增 session 配置段(idle_timeout/remember_timeout/cleanup_interval) - TokenService/auth_parser/auth_token 标记废弃,移除 JWT 到期字段依赖
This commit is contained in:
@ -3,14 +3,17 @@ package service
|
||||
import (
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/session"
|
||||
)
|
||||
|
||||
type AdminService struct {
|
||||
userRepo userAdminStore
|
||||
userRepo userAdminStore
|
||||
sessionManager *session.Manager
|
||||
}
|
||||
|
||||
// NewAdminService 构造函数
|
||||
func NewAdminService(userRepo userAdminStore) *AdminService {
|
||||
return &AdminService{userRepo: userRepo}
|
||||
func NewAdminService(userRepo userAdminStore, sm *session.Manager) *AdminService {
|
||||
return &AdminService{userRepo: userRepo, sessionManager: sm}
|
||||
}
|
||||
|
||||
// ListUsersParams 用户列表查询参数
|
||||
@ -92,14 +95,20 @@ func (s *AdminService) UpdateUserStatus(operatorUID, targetUID uint, newStatus s
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 修改状态必须立刻让 JWT 失效
|
||||
// 修改状态必须立刻让已有 session 失效 + 递增 token_version(向后兼容)
|
||||
if err := s.sessionManager.DestroyByUID(target.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.userRepo.IncrementTokenVersion(target.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// ResetUserToken 强制下线(递增 token_version)
|
||||
// ResetUserToken 强制下线:销毁所有 session + 递增 token_version
|
||||
func (s *AdminService) ResetUserToken(operatorUID, targetUID uint) error {
|
||||
return s.checkAndOperate(operatorUID, targetUID, func(_ *model.User, target *model.User) error {
|
||||
if err := s.sessionManager.DestroyByUID(target.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.userRepo.IncrementTokenVersion(target.ID)
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/config"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
"metazone.cc/metalab/internal/session"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
@ -15,15 +16,15 @@ import (
|
||||
|
||||
// AuthService 认证业务逻辑
|
||||
type AuthService struct {
|
||||
userRepo userAuthStore
|
||||
tokenService tokenProvider
|
||||
cfg *config.Config
|
||||
siteSettings *config.SiteSettings
|
||||
userRepo userAuthStore
|
||||
sessionManager *session.Manager
|
||||
cfg *config.Config
|
||||
siteSettings *config.SiteSettings
|
||||
}
|
||||
|
||||
// NewAuthService 构造函数
|
||||
func NewAuthService(userRepo userAuthStore, tokenSvc tokenProvider, cfg *config.Config, siteSettings *config.SiteSettings) *AuthService {
|
||||
return &AuthService{userRepo: userRepo, tokenService: tokenSvc, cfg: cfg, siteSettings: siteSettings}
|
||||
func NewAuthService(userRepo userAuthStore, sm *session.Manager, cfg *config.Config, siteSettings *config.SiteSettings) *AuthService {
|
||||
return &AuthService{userRepo: userRepo, sessionManager: sm, cfg: cfg, siteSettings: siteSettings}
|
||||
}
|
||||
|
||||
var pwLetter = regexp.MustCompile(`[a-zA-Z]`)
|
||||
@ -33,7 +34,6 @@ var pwDigit = regexp.MustCompile(`\d`)
|
||||
var usernamePattern = regexp.MustCompile(`^[\p{Han}a-zA-Z0-9_-]+$`)
|
||||
|
||||
// dummyHash 防时序攻击:当用户不存在时,仍对虚拟哈希执行完整的 bcrypt 比对
|
||||
// 在 init() 中按当前 bcrypt 成本生成,确保时序与真实校验一致
|
||||
var dummyHash []byte
|
||||
|
||||
func init() {
|
||||
@ -58,46 +58,44 @@ func (s *AuthService) IsRegistrationEnabled() bool {
|
||||
}
|
||||
|
||||
// Register 注册
|
||||
// 流程:密码强度 → 邮箱查重 → 哈希 → 生成唯一用户名 → 创建 → access JWT + refresh JWT
|
||||
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
||||
// regIP: 注册 IP 地址
|
||||
func (s *AuthService) Register(req model.RegisterRequest, regIP string) (string, string, *model.User, error) {
|
||||
// 流程:密码强度 → 邮箱查重 → 哈希 → 生成唯一用户名 → 创建 → 返回 user(session 由 controller 创建)
|
||||
func (s *AuthService) Register(req model.RegisterRequest, regIP string) (*model.User, error) {
|
||||
// 0. 维护期间禁止注册
|
||||
if s.siteSettings.IsMaintenanceEnabled() {
|
||||
return "", "", nil, common.ErrMaintenanceMode
|
||||
return nil, common.ErrMaintenanceMode
|
||||
}
|
||||
// 1. 检查注册开关
|
||||
if !s.siteSettings.IsRegistrationEnabled() {
|
||||
return "", "", nil, common.ErrRegistrationDisabled
|
||||
return nil, common.ErrRegistrationDisabled
|
||||
}
|
||||
|
||||
// 1. 密码强度
|
||||
// 2. 密码强度
|
||||
if err := validatePassword(req.Password); err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 检查邮箱
|
||||
// 3. 检查邮箱
|
||||
exists, err := s.userRepo.ExistsByEmail(req.Email)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return "", "", nil, common.ErrEmailExists
|
||||
return nil, common.ErrEmailExists
|
||||
}
|
||||
|
||||
// 3. 哈希密码
|
||||
// 4. 哈希密码
|
||||
hash, err := common.HashPassword(req.Password, s.cfg.Bcrypt.Cost)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 生成唯一用户名
|
||||
// 5. 生成唯一用户名
|
||||
username, err := common.GenerateUsername(s.userRepo, 20)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 创建用户
|
||||
// 6. 创建用户
|
||||
user := &model.User{
|
||||
Email: req.Email,
|
||||
PasswordHash: hash,
|
||||
@ -107,70 +105,56 @@ func (s *AuthService) Register(req model.RegisterRequest, regIP string) (string,
|
||||
RegIP: regIP,
|
||||
}
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return "", "", nil, err
|
||||
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
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
// 流程:按邮箱查找 → 维护期间非站长统一拦截(模拟 bcrypt 防时序)→ 状态检查 → 验证密码 → 签发 token
|
||||
// rememberMe=true: refresh Cookie 持久化(30天);false: session cookie(关浏览器即清除)
|
||||
func (s *AuthService) Login(req model.LoginRequest, loginIP string) (string, string, *model.User, error) {
|
||||
// 流程:按邮箱查找 → 维护期间非站长统一拦截 → 状态检查 → 验证密码 → 记录登录信息 → 返回 user
|
||||
func (s *AuthService) Login(req model.LoginRequest, loginIP string) (*model.User, error) {
|
||||
user, err := s.userRepo.FindByEmail(req.Email)
|
||||
|
||||
// 维护期间:非站长一律返回统一提示(模拟 bcrypt 防时序攻击,不区分账号是否存在)
|
||||
// 维护期间:非站长一律返回统一提示(模拟 bcrypt 防时序攻击)
|
||||
if s.siteSettings.IsMaintenanceEnabled() {
|
||||
if err != nil {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrMaintenanceMode
|
||||
return nil, common.ErrMaintenanceMode
|
||||
}
|
||||
if !model.HasMinRole(user.Role, model.RoleOwner) {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrMaintenanceMode
|
||||
return nil, common.ErrMaintenanceMode
|
||||
}
|
||||
// 站长 → 继续正常登录流程
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrInvalidCred
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
|
||||
// 已注销(deleted)→ 验证密码后要求二次确认,不自动恢复
|
||||
// 已注销(deleted)→ 验证密码后要求二次确认
|
||||
if user.Status == model.StatusDeleted {
|
||||
if !common.CheckPassword(req.Password, user.PasswordHash) {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrInvalidCred
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
return "", "", nil, common.ErrNeedsConfirmRestore
|
||||
return nil, common.ErrNeedsConfirmRestore
|
||||
}
|
||||
|
||||
// 永久锁定
|
||||
if user.Status == model.StatusLocked {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrUserLocked
|
||||
return nil, common.ErrUserLocked
|
||||
}
|
||||
|
||||
// 封禁
|
||||
if user.Status == model.StatusBanned {
|
||||
return "", "", nil, common.ErrUserBanned
|
||||
return nil, common.ErrUserBanned
|
||||
}
|
||||
|
||||
if !common.CheckPassword(req.Password, user.PasswordHash) {
|
||||
return "", "", nil, common.ErrInvalidCred
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
|
||||
// 记录登录 IP 和时间
|
||||
@ -178,40 +162,26 @@ func (s *AuthService) Login(req model.LoginRequest, loginIP string) (string, str
|
||||
user.LastLoginIP = loginIP
|
||||
user.LastLoginAt = &now
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成 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
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ConfirmRestore 二次确认恢复已注销账号
|
||||
// 流程:按邮箱查找 → 验证密码 → 恢复为 active → 记录登录 IP/时间 → 签发 token
|
||||
func (s *AuthService) ConfirmRestore(req model.LoginRequest, loginIP string) (string, string, *model.User, error) {
|
||||
func (s *AuthService) ConfirmRestore(req model.LoginRequest, loginIP string) (*model.User, error) {
|
||||
user, err := s.userRepo.FindByEmail(req.Email)
|
||||
if err != nil {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte(req.Password))
|
||||
return "", "", nil, common.ErrInvalidCred
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
|
||||
// 仅 deleted 状态允许恢复
|
||||
if user.Status != model.StatusDeleted {
|
||||
return "", "", nil, common.ErrUserNotFound
|
||||
return nil, common.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !common.CheckPassword(req.Password, user.PasswordHash) {
|
||||
return "", "", nil, common.ErrInvalidCred
|
||||
return nil, common.ErrInvalidCred
|
||||
}
|
||||
|
||||
// 恢复账号
|
||||
@ -221,51 +191,38 @@ func (s *AuthService) ConfirmRestore(req model.LoginRequest, loginIP string) (st
|
||||
user.LastLoginIP = loginIP
|
||||
user.LastLoginAt = &now
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, err := s.tokenService.BuildAccessToken(user)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.tokenService.BuildRefreshToken(user, req.RememberMe)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CheckEmail 检查邮箱是否已被注册(无需认证的轻量检查)
|
||||
// CheckEmail 检查邮箱是否已被注册
|
||||
func (s *AuthService) CheckEmail(email string) (bool, error) {
|
||||
return s.userRepo.ExistsByEmail(email)
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户资料(供 SettingsController 使用)
|
||||
// GetProfile 获取当前用户资料
|
||||
func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
|
||||
return s.userRepo.FindByID(userID)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 吊销所有 JWT(强制重新登录)
|
||||
// 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 删除所有 session(强制重新登录)
|
||||
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
if !common.CheckPassword(currentPassword, user.PasswordHash) {
|
||||
return common.ErrIncorrectPassword
|
||||
}
|
||||
|
||||
// 新密码强度校验
|
||||
if err := validatePassword(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 哈希新密码
|
||||
hash, err := common.HashPassword(newPassword, s.cfg.Bcrypt.Cost)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -276,24 +233,21 @@ func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword s
|
||||
return err
|
||||
}
|
||||
|
||||
// 强制所有设备重新登录(安全性)
|
||||
return s.InvalidateSessions(userID)
|
||||
// 删除所有 session,强制所有设备重新登录
|
||||
return s.invalidateSessions(userID)
|
||||
}
|
||||
|
||||
// DeleteAccount 用户自主注销
|
||||
// 流程:检查角色 → 验证密码 → 记录原因 → 设置 deleted 状态 → 吊销所有 JWT → 7 天冷却期内登录需二次确认恢复
|
||||
func (s *AuthService) DeleteAccount(userID uint, password, reason string) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
}
|
||||
|
||||
// 站长不允许自主注销(避免权限体系死锁)
|
||||
if user.Role == model.RoleOwner {
|
||||
return common.ErrOwnerCannotDelete
|
||||
}
|
||||
|
||||
// 验证当前密码(防止 CSRF 或未授权操作)
|
||||
if !common.CheckPassword(password, user.PasswordHash) {
|
||||
return common.ErrIncorrectPassword
|
||||
}
|
||||
@ -305,22 +259,28 @@ func (s *AuthService) DeleteAccount(userID uint, password, reason string) error
|
||||
return err
|
||||
}
|
||||
|
||||
// 递增 token_version,即时吊销所有 JWT 强制退登
|
||||
return s.InvalidateSessions(userID)
|
||||
}
|
||||
|
||||
// InvalidateSessions 吊销某用户所有 JWT(递增 token_version,强制所有设备重新登录)
|
||||
// 适用场景:修改密码、账号被盗、管理员强制下线
|
||||
func (s *AuthService) InvalidateSessions(userID uint) error {
|
||||
// 删除所有 session + 递增 token_version(向后兼容)
|
||||
if err := s.invalidateSessions(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.userRepo.IncrementTokenVersion(userID)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改个人资料(用户名 + 个性签名)
|
||||
// 用户名校验:非空、1-16 字符、白名单、去重
|
||||
// 个性签名校验:0-128 字符纯文本,Go 模板自动 HTML 转义防 XSS
|
||||
// 任一字段无变更时跳过该字段的写库操作
|
||||
// invalidateSessions 销毁某用户的所有 session(内部方法)
|
||||
func (s *AuthService) invalidateSessions(userID uint) error {
|
||||
return s.sessionManager.DestroyByUID(userID)
|
||||
}
|
||||
|
||||
// InvalidateSessions 公开方法:销毁用户所有 session(供 admin 等外部调用)
|
||||
func (s *AuthService) InvalidateSessions(userID uint) error {
|
||||
if err := s.invalidateSessions(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.userRepo.IncrementTokenVersion(userID)
|
||||
}
|
||||
|
||||
// UpdateProfile 修改个人资料
|
||||
func (s *AuthService) UpdateProfile(userID uint, username, bio string) error {
|
||||
// 查当前用户
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
@ -328,7 +288,6 @@ func (s *AuthService) UpdateProfile(userID uint, username, bio string) error {
|
||||
|
||||
needsUpdate := false
|
||||
|
||||
// 用户名:校验 + 更新
|
||||
if n := utf8.RuneCountInString(username); n == 0 {
|
||||
return common.ErrUsernameInvalid
|
||||
} else if n > 16 {
|
||||
@ -349,7 +308,6 @@ func (s *AuthService) UpdateProfile(userID uint, username, bio string) error {
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// 个性签名:长度校验 + 更新
|
||||
if utf8.RuneCountInString(bio) > 128 {
|
||||
return common.ErrBioTooLong
|
||||
}
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
package service
|
||||
|
||||
import "metazone.cc/metalab/internal/model"
|
||||
|
||||
// tokenProvider AuthService 对 TokenService 的最小依赖(ISP:2 个方法)
|
||||
type tokenProvider interface {
|
||||
BuildAccessToken(user *model.User) (string, error)
|
||||
BuildRefreshToken(user *model.User, rememberMe bool) (string, error)
|
||||
}
|
||||
// provider.go: tokenProvider 接口已移除,JWT 被服务端 Session 替换。
|
||||
// 本文件保留占位,后续清理。
|
||||
|
||||
@ -2,8 +2,7 @@ package service
|
||||
|
||||
import "metazone.cc/metalab/internal/model"
|
||||
|
||||
// userAuthStore AuthService 所需的最小仓储接口(ISP:7 个方法)
|
||||
// 同时满足 common.UsernameChecker(ExistsByUsername),可传入 GenerateUsername
|
||||
// userAuthStore AuthService 所需的最小仓储接口(ISP:8 个方法)
|
||||
type userAuthStore interface {
|
||||
ExistsByEmail(email string) (bool, error)
|
||||
Create(user *model.User) error
|
||||
@ -14,12 +13,12 @@ type userAuthStore interface {
|
||||
ExistsByUsername(username string) (bool, error)
|
||||
}
|
||||
|
||||
// userTokenStore TokenService 所需的最小仓储接口(ISP:1 个方法)
|
||||
// userTokenStore 已废弃(JWT 替换为 Session),保留占位
|
||||
type userTokenStore interface {
|
||||
FindByIDForAuth(userID uint) (*model.User, error)
|
||||
}
|
||||
|
||||
// userAdminStore AdminService 所需的最小仓储接口(ISP:8 个方法)
|
||||
// userAdminStore AdminService 所需的最小仓储接口
|
||||
type userAdminStore interface {
|
||||
CountSearchUsers(keyword, role, status string) (int64, error)
|
||||
SearchUsers(keyword, role, status string, offset, limit int) ([]model.User, error)
|
||||
@ -31,7 +30,7 @@ type userAdminStore interface {
|
||||
IncrementTokenVersion(userID uint) error
|
||||
}
|
||||
|
||||
// postStore PostService 所需的最小仓储接口(ISP:10 个方法)
|
||||
// postStore PostService 所需的最小仓储接口
|
||||
type postStore interface {
|
||||
Create(post *model.Post) error
|
||||
FindByID(id uint) (*model.Post, error)
|
||||
@ -45,18 +44,18 @@ type postStore interface {
|
||||
Restore(id uint) error
|
||||
}
|
||||
|
||||
// spaceUserStore SpaceService 所需的最小用户仓储接口(ISP:1 个方法)
|
||||
// spaceUserStore SpaceService 所需的最小用户仓储接口
|
||||
type spaceUserStore interface {
|
||||
FindByID(id uint) (*model.User, error)
|
||||
}
|
||||
|
||||
// spacePostStore SpaceService 所需的最小帖子仓储接口(ISP:1 个方法)
|
||||
// spacePostStore SpaceService 所需的最小帖子仓储接口
|
||||
type spacePostStore interface {
|
||||
FindByUserID(userID uint, offset, limit int) ([]model.Post, int64, error)
|
||||
}
|
||||
|
||||
// postNotifier PostService 所需的通知服务接口(ISP:2 个方法)
|
||||
// postNotifier PostService 所需的通知服务接口
|
||||
type postNotifier interface {
|
||||
NotifyPostApproved(userID uint, postTitle string, postID uint)
|
||||
NotifyPostRejected(userID uint, postTitle, reason string, postID uint)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,122 +1,3 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"metazone.cc/metalab/internal/common"
|
||||
"metazone.cc/metalab/internal/config"
|
||||
"metazone.cc/metalab/internal/model"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// TokenService JWT 令牌签发与刷新
|
||||
// 独立于认证业务逻辑,供 AuthService 和其他需要签发令牌的服务使用
|
||||
type TokenService struct {
|
||||
cfg *config.Config
|
||||
userRepo userTokenStore
|
||||
}
|
||||
|
||||
// NewTokenService 构造函数
|
||||
func NewTokenService(cfg *config.Config, userRepo userTokenStore) *TokenService {
|
||||
return &TokenService{cfg: cfg, userRepo: userRepo}
|
||||
}
|
||||
|
||||
// BuildAccessToken 构建 access JWT(含 uid/email/username/role/ver/exp/iat)
|
||||
// ver = user.TokenVersion,用于即时吊销:版本号不匹配则拒绝
|
||||
func (ts *TokenService) BuildAccessToken(user *model.User) (string, error) {
|
||||
expire := time.Duration(ts.cfg.JWT.AccessExpire) * time.Minute
|
||||
now := time.Now()
|
||||
expAt := now.Add(expire)
|
||||
log.Printf("[BuildAccessToken] AccessExpire=%d min → expire=%v | now=%v | exp=%v (%d) | iat=%d ver=%d",
|
||||
ts.cfg.JWT.AccessExpire, expire, now, expAt, expAt.Unix(), now.Unix(), user.TokenVersion)
|
||||
claims := jwt.MapClaims{
|
||||
"uid": user.ID,
|
||||
"email": user.Email,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"ver": user.TokenVersion,
|
||||
"exp": expAt.Unix(),
|
||||
"iat": now.Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(ts.cfg.JWT.Secret))
|
||||
}
|
||||
|
||||
// BuildRefreshToken 构建 refresh JWT(含 purpose:"refresh" 防止 access 冒充)
|
||||
// rememberMe=true → 使用 remember_expire(30天);false → 使用 refresh_expire(7天,session 模式)
|
||||
// ver = user.TokenVersion,刷新时校验:版本号不匹配则拒绝
|
||||
func (ts *TokenService) BuildRefreshToken(user *model.User, rememberMe bool) (string, error) {
|
||||
var expireHours int
|
||||
if rememberMe {
|
||||
expireHours = ts.cfg.JWT.RememberExpire
|
||||
} else {
|
||||
expireHours = ts.cfg.JWT.RefreshExpire
|
||||
}
|
||||
expire := time.Duration(expireHours) * time.Hour
|
||||
now := time.Now()
|
||||
claims := jwt.MapClaims{
|
||||
"uid": user.ID,
|
||||
"email": user.Email,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"ver": user.TokenVersion,
|
||||
"exp": now.Add(expire).Unix(),
|
||||
"iat": now.Unix(),
|
||||
"purpose": "refresh",
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(ts.cfg.JWT.Secret))
|
||||
}
|
||||
|
||||
// RefreshAccessToken 用 refresh token 换取新的 access token
|
||||
// 校验 token_version:若 DB 中版本已递增,拒绝刷新 → 即时吊销
|
||||
func (ts *TokenService) RefreshAccessToken(refreshTokenStr string) (string, *model.User, error) {
|
||||
if refreshTokenStr == "" {
|
||||
return "", nil, common.ErrTokenInvalid
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(refreshTokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(ts.cfg.JWT.Secret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return "", nil, common.ErrTokenExpired
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return "", nil, common.ErrTokenInvalid
|
||||
}
|
||||
|
||||
// 只接受 refresh 用途的 token,防止 access token 被用于刷新
|
||||
if purpose, _ := claims["purpose"].(string); purpose != "refresh" {
|
||||
return "", nil, common.ErrTokenInvalid
|
||||
}
|
||||
|
||||
uid := uint(claims["uid"].(float64))
|
||||
tokenVer := int(claims["ver"].(float64))
|
||||
|
||||
// 即时吊销检查 + 获取最新用户数据(角色/状态可能在 JWT 签发后已变更)
|
||||
// 用 FindByIDForAuth 而非从 claims 重建,确保 access token 承载最新数据
|
||||
user, err := ts.userRepo.FindByIDForAuth(uid)
|
||||
if err != nil {
|
||||
return "", nil, common.ErrTokenRevoked
|
||||
}
|
||||
|
||||
if tokenVer != user.TokenVersion {
|
||||
log.Printf("[RefreshAccessToken] REVOKED: uid=%d tokenVer=%d dbVer=%d", uid, tokenVer, user.TokenVersion)
|
||||
return "", nil, common.ErrTokenRevoked
|
||||
}
|
||||
|
||||
// 防止封禁用户通过 refresh 续期
|
||||
if user.Status == model.StatusBanned {
|
||||
return "", nil, common.ErrUserBanned
|
||||
}
|
||||
|
||||
accessToken, err := ts.BuildAccessToken(user)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return accessToken, user, nil
|
||||
}
|
||||
// token_service.go: JWT 签发与刷新逻辑已被服务端 Session 替换,本文件保留占位,后续清理。
|
||||
|
||||
Reference in New Issue
Block a user