// Package auth JWT 签发/校验 + jti 黑名单。微信 OAuth 在 wechat.go(5B/5C)。 package auth import ( "context" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/redis/go-redis/v9" "dudu/server/internal/store" "dudu/server/pkg/protocol" ) type JWT struct { secret []byte ttl time.Duration rdb *redis.Client } func NewJWT(secret string, ttl time.Duration, rdb *redis.Client) *JWT { return &JWT{secret: []byte(secret), ttl: ttl, rdb: rdb} } type Claims struct { jwt.RegisteredClaims } func (j *JWT) Sign(userID string) (string, error) { now := time.Now() claims := Claims{RegisteredClaims: jwt.RegisteredClaims{ Subject: userID, ID: uuid.NewString(), IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)), }} return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.secret) } // Parse 校验签名、过期与黑名单,返回 userID 与 jti。 func (j *JWT) Parse(ctx context.Context, token string) (userID, jti string, err error) { var claims Claims _, err = jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (any, error) { return j.secret, nil }, jwt.WithValidMethods([]string{"HS256"})) if err != nil { return "", "", err } if j.rdb != nil { if n, _ := j.rdb.Exists(ctx, store.KeyJwtBlock(claims.ID)).Result(); n == 1 { return "", "", jwt.ErrTokenExpired } } return claims.Subject, claims.ID, nil } // Revoke 将 jti 拉黑至 token 自然过期(logout / 踢出)。 func (j *JWT) Revoke(ctx context.Context, jti string, expiresAt time.Time) error { ttl := time.Until(expiresAt) if ttl <= 0 { return nil } return j.rdb.Set(ctx, store.KeyJwtBlock(jti), 1, ttl).Err() } const ( CtxUserID = "auth.user_id" CtxJTI = "auth.jti" ) // Middleware gin 鉴权中间件:Authorization: Bearer 。 func (j *JWT) Middleware() gin.HandlerFunc { return func(c *gin.Context) { token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") if token == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized)) return } uid, jti, err := j.Parse(c.Request.Context(), token) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized)) return } c.Set(CtxUserID, uid) c.Set(CtxJTI, jti) c.Next() } } // UserID 从 gin 上下文取当前用户。 func UserID(c *gin.Context) string { return c.GetString(CtxUserID) }