Files
mce/internal/service/token_service.go
Victor_Jay 39d13993ba fix: 设计原则审查修复 — DIP/ISP, LoD, DRY, OCP, URL, 301缓存
- P0 DIP+ISP: 全链路注入接口,消除零接口紧耦合
- P0 URL: auth 301→302,修复登出后浏览器缓存陷阱
- P1 DRY: JWT 认证逻辑收敛至 TokenService+中间件
- P2 DRY: 前后端角色/状态映射统一为 model 常量
- P2 LoD: 新增 SettingsController,router 不再跨层调 repo
- P2 URL: settings ?tab= → /settings/:tab 伪静态
- P3 OCP: 角色权限 map 化,告别硬编码 switch
2026-05-26 21:12:19 +08:00

118 lines
3.8 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 (
"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_expire30天false → 使用 refresh_expire7天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
}
accessToken, err := ts.BuildAccessToken(user)
if err != nil {
return "", nil, err
}
return accessToken, user, nil
}