jwt.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. package auth
  2. import (
  3. "errors"
  4. "fmt"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/middleware"
  8. "github.com/golang-jwt/jwt/v4"
  9. )
  10. var ErrTokenTypeMismatch = errors.New("token type mismatch")
  11. // ParseWithHMAC 统一的 keyfunc 断言入口。所有 JWT 解析点(HTTP 中间件 / gRPC VerifyToken / RefreshToken)
  12. // 都必须走这里,而不是直接 jwt.ParseWithClaims(... func() {return []byte(secret)}) ——
  13. // 必须显式断言 token.Method 是 *jwt.SigningMethodHMAC,避免未来迁移到 RSA/ECDSA 非对称密钥
  14. // 时,攻击者把公钥当成 HMAC 共享密钥伪造 token(jwt-go 历史上 CVE-2016-10555 同类问题,
  15. // OWASP JWT Cheat Sheet / RFC 8725 均强制要求 alg 白名单,见审计 H-4)。
  16. //
  17. // alg=none 在 jwt-go v4 早已默认拒绝,但显式 method 断言仍是深度防御的必要一步:
  18. // 即使未来有人误签出 alg=HS512 的 token,这里也会直接报错而不是当成 HS256 尝试解析。
  19. func ParseWithHMAC(tokenStr, secret string, claims jwt.Claims) (*jwt.Token, error) {
  20. return jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
  21. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  22. return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
  23. }
  24. return []byte(secret), nil
  25. })
  26. }
  27. type RefreshClaims struct {
  28. TokenType string `json:"tokenType"`
  29. UserId int64 `json:"userId"`
  30. ProductCode string `json:"productCode"`
  31. TokenVersion int64 `json:"tokenVersion"`
  32. jwt.RegisteredClaims
  33. }
  34. func GenerateAccessToken(secret string, expireSeconds int64, userId int64, username, productCode, memberType string, tokenVersion int64) (string, error) {
  35. now := time.Now()
  36. claims := middleware.Claims{
  37. TokenType: consts.TokenTypeAccess,
  38. UserId: userId,
  39. Username: username,
  40. ProductCode: productCode,
  41. MemberType: memberType,
  42. TokenVersion: tokenVersion,
  43. RegisteredClaims: jwt.RegisteredClaims{
  44. ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)),
  45. IssuedAt: jwt.NewNumericDate(now),
  46. },
  47. }
  48. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  49. return token.SignedString([]byte(secret))
  50. }
  51. func GenerateRefreshToken(secret string, expireSeconds int64, userId int64, productCode string, tokenVersion int64) (string, error) {
  52. now := time.Now()
  53. claims := RefreshClaims{
  54. TokenType: consts.TokenTypeRefresh,
  55. UserId: userId,
  56. ProductCode: productCode,
  57. TokenVersion: tokenVersion,
  58. RegisteredClaims: jwt.RegisteredClaims{
  59. ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expireSeconds) * time.Second)),
  60. IssuedAt: jwt.NewNumericDate(now),
  61. },
  62. }
  63. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  64. return token.SignedString([]byte(secret))
  65. }
  66. // GenerateRefreshTokenWithExpiry 签发 refreshToken,使用绝对过期时间(用于 token 轮转场景)。
  67. func GenerateRefreshTokenWithExpiry(secret string, expiresAt time.Time, userId int64, productCode string, tokenVersion int64) (string, error) {
  68. now := time.Now()
  69. if !expiresAt.After(now) {
  70. return "", errors.New("refresh token has expired")
  71. }
  72. claims := RefreshClaims{
  73. TokenType: consts.TokenTypeRefresh,
  74. UserId: userId,
  75. ProductCode: productCode,
  76. TokenVersion: tokenVersion,
  77. RegisteredClaims: jwt.RegisteredClaims{
  78. ExpiresAt: jwt.NewNumericDate(expiresAt),
  79. IssuedAt: jwt.NewNumericDate(now),
  80. },
  81. }
  82. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
  83. return token.SignedString([]byte(secret))
  84. }
  85. func ParseRefreshToken(tokenStr, secret string) (*RefreshClaims, error) {
  86. token, err := ParseWithHMAC(tokenStr, secret, &RefreshClaims{})
  87. if err != nil {
  88. return nil, err
  89. }
  90. claims, ok := token.Claims.(*RefreshClaims)
  91. if !ok || !token.Valid {
  92. return nil, jwt.ErrSignatureInvalid
  93. }
  94. if claims.TokenType != consts.TokenTypeRefresh {
  95. return nil, ErrTokenTypeMismatch
  96. }
  97. return claims, nil
  98. }