package auth import ( "errors" "fmt" "time" "perms-system-server/internal/consts" "perms-system-server/internal/middleware" "github.com/golang-jwt/jwt/v4" ) var ErrTokenTypeMismatch = errors.New("token type mismatch") // ParseWithHMAC 统一的 keyfunc 断言入口。所有 JWT 解析点(HTTP 中间件 / gRPC VerifyToken / RefreshToken) // 都必须走这里,而不是直接 jwt.ParseWithClaims(... func() {return []byte(secret)}) —— // 必须显式断言 token.Method 是 *jwt.SigningMethodHMAC,避免未来迁移到 RSA/ECDSA 非对称密钥 // 时,攻击者把公钥当成 HMAC 共享密钥伪造 token(jwt-go 历史上 CVE-2016-10555 同类问题, // OWASP JWT Cheat Sheet / RFC 8725 均强制要求 alg 白名单,见审计 H-4)。 // // alg=none 在 jwt-go v4 早已默认拒绝,但显式 method 断言仍是深度防御的必要一步: // 即使未来有人误签出 alg=HS512 的 token,这里也会直接报错而不是当成 HS256 尝试解析。 func ParseWithHMAC(tokenStr, secret string, claims jwt.Claims) (*jwt.Token, error) { return jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(secret), nil }) } type RefreshClaims struct { TokenType string `json:"tokenType"` UserId int64 `json:"userId"` ProductCode string `json:"productCode"` TokenVersion int64 `json:"tokenVersion"` jwt.RegisteredClaims } func GenerateAccessToken(secret string, expireSeconds int64, userId int64, username, productCode, memberType string, tokenVersion int64) (string, error) { now := time.Now() claims := middleware.Claims{ TokenType: consts.TokenTypeAccess, UserId: userId, Username: username, ProductCode: productCode, MemberType: memberType, TokenVersion: tokenVersion, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)), IssuedAt: jwt.NewNumericDate(now), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } func GenerateRefreshToken(secret string, expireSeconds int64, userId int64, productCode string, tokenVersion int64) (string, error) { now := time.Now() claims := RefreshClaims{ TokenType: consts.TokenTypeRefresh, UserId: userId, ProductCode: productCode, TokenVersion: tokenVersion, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)), IssuedAt: jwt.NewNumericDate(now), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } // GenerateRefreshTokenWithExpiry 签发 refreshToken,使用绝对过期时间(用于 token 轮转场景)。 func GenerateRefreshTokenWithExpiry(secret string, expiresAt time.Time, userId int64, productCode string, tokenVersion int64) (string, error) { now := time.Now() if !expiresAt.After(now) { return "", errors.New("refresh token has expired") } claims := RefreshClaims{ TokenType: consts.TokenTypeRefresh, UserId: userId, ProductCode: productCode, TokenVersion: tokenVersion, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(now), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } func ParseRefreshToken(tokenStr, secret string) (*RefreshClaims, error) { token, err := ParseWithHMAC(tokenStr, secret, &RefreshClaims{}) if err != nil { return nil, err } claims, ok := token.Claims.(*RefreshClaims) if !ok || !token.Valid { return nil, jwt.ErrSignatureInvalid } if claims.TokenType != consts.TokenTypeRefresh { return nil, ErrTokenTypeMismatch } return claims, nil }