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/middleware/auth.go

147 lines
4.3 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 middleware
import (
"log"
"strings"
"time"
"metazone.cc/metalab/internal/common"
"metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/repository"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// AuthMiddleware 认证中间件(结构体模式,持有 DB 依赖用于实时令牌吊销校验)
type AuthMiddleware struct {
cfg *config.Config
userRepo *repository.UserRepo
}
// NewAuthMiddleware 构造函数
func NewAuthMiddleware(cfg *config.Config, userRepo *repository.UserRepo) *AuthMiddleware {
return &AuthMiddleware{cfg: cfg, userRepo: userRepo}
}
// Required 登录认证中间件:校验 JWT → 检查 token_version → 注入用户信息
// 令牌版本与 DB 不匹配时即时拒绝,实现实时吊销
func (am *AuthMiddleware) Required() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr, err := c.Cookie(common.CookieName)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{
"success": false, "message": "请先登录",
})
return
}
claims, err := parseToken(tokenStr, am.cfg.JWT.Secret)
if err != nil {
common.ClearAuthCookies(c, am.cfg)
c.AbortWithStatusJSON(401, gin.H{
"success": false, "message": "登录已过期,请重新登录",
})
return
}
uid := uint(claims["uid"].(float64))
tokenVer := int(claims["ver"].(float64))
// 即时吊销检查:查询 DB 当前版本号,不匹配则拒绝
currentVer, err := am.userRepo.FindTokenVersion(uid)
if err != nil || tokenVer != currentVer {
if tokenVer != currentVer {
log.Printf("[AuthRequired] REVOKED: uid=%d tokenVer=%d dbVer=%d", uid, tokenVer, currentVer)
}
common.ClearAuthCookies(c, am.cfg)
c.AbortWithStatusJSON(401, gin.H{
"success": false, "message": "登录凭证已失效,请重新登录",
})
return
}
// 注入用户信息到上下文
c.Set("uid", uid)
c.Set("email", claims["email"])
c.Set("username", claims["username"])
c.Set("role", claims["role"])
c.Next()
}
}
// Optional 可选认证:已登录且版本通过则注入,未登录或版本不匹配也放行(仅清除过期 Cookie
func (am *AuthMiddleware) Optional() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr, err := c.Cookie(common.CookieName)
if err != nil {
log.Println("[AuthOptional] no cookie for", c.Request.URL.Path)
c.Next()
return
}
claims, err := parseToken(tokenStr, am.cfg.JWT.Secret)
if err != nil {
log.Printf("[AuthOptional] JWT parse failed for %s: %v", c.Request.URL.Path, err)
c.Next()
return
}
uid := uint(claims["uid"].(float64))
tokenVer := int(claims["ver"].(float64))
// 即时吊销检查
currentVer, err := am.userRepo.FindTokenVersion(uid)
if err != nil || tokenVer != currentVer {
log.Printf("[AuthOptional] REVOKED or err: uid=%d tokenVer=%d dbVer=%d err=%v",
uid, tokenVer, currentVer, err)
// 已吊销:清除 Cookie但仍放行页面视图前端自行处理
c.Next()
return
}
log.Printf("[AuthOptional] OK: uid=%v username=%v path=%s", claims["uid"], claims["username"], c.Request.URL.Path)
c.Set("uid", uid)
c.Set("email", claims["email"])
c.Set("username", claims["username"])
c.Set("role", claims["role"])
c.Next()
}
}
// parseToken 解析并验证 JWT
func parseToken(tokenStr, secret string) (jwt.MapClaims, error) {
now := time.Now()
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil || !token.Valid {
log.Printf("[parseToken] FAIL: now=%v err=%v (secret_len=%d token_len=%d)",
now, err, len(secret), len(tokenStr))
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, jwt.ErrSignatureInvalid
}
// Debug: 打印 exp/iat 对比当前时间
if exp, exists := claims["exp"]; exists {
var expTime time.Time
switch v := exp.(type) {
case float64:
expTime = time.Unix(int64(v), 0)
case *jwt.NumericDate:
expTime = v.Time
}
if !expTime.IsZero() {
log.Printf("[parseToken] OK: exp=%v (%d) now=%v isExpired=%v",
expTime, expTime.Unix(), now, now.After(expTime))
}
}
return claims, nil
}
// IsLoginPage 检查是否已在登录状态,已登录用户跳过登录/注册页
func IsLoginPage(c *gin.Context) bool {
return strings.HasPrefix(c.Request.URL.Path, "/auth/")
}