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:
@ -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