124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
package service
|
||
|
||
import (
|
||
"log"
|
||
"time"
|
||
|
||
"metazone.cc/metalab/internal/common"
|
||
"metazone.cc/metalab/internal/config"
|
||
"metazone.cc/metalab/internal/model"
|
||
"metazone.cc/metalab/internal/repository"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
// TokenService JWT 令牌签发与刷新
|
||
// 独立于认证业务逻辑,供 AuthService 和其他需要签发令牌的服务使用
|
||
type TokenService struct {
|
||
cfg *config.Config
|
||
userRepo *repository.UserRepo
|
||
}
|
||
|
||
// NewTokenService 构造函数
|
||
func NewTokenService(cfg *config.Config, userRepo *repository.UserRepo) *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, 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, ErrTokenExpired
|
||
}
|
||
|
||
claims, ok := token.Claims.(jwt.MapClaims)
|
||
if !ok {
|
||
return "", nil, ErrTokenInvalid
|
||
}
|
||
|
||
// 只接受 refresh 用途的 token,防止 access token 被用于刷新
|
||
if purpose, _ := claims["purpose"].(string); purpose != "refresh" {
|
||
return "", nil, 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
|
||
}
|