refactor(auth): migrate JWT handling to v5

Use RegisteredClaims and ParseWithClaims, restrict accepted tokens to HS256, and keep existing claim names and signatures wire-compatible.

Add regression coverage for generated and legacy tokens, expiry validation, and algorithm rejection.
This commit is contained in:
qwqcode committed 2026-07-22 22:13:44 +08:00
1 parent 5ff42483aa
commit f7a2aeac9b
4 files changed
+89 -19

No files matched your search

+1 -2
View File
@@ -20,7 +20,7 @@ require (
github.com/goccy/go-yaml v1.19.2
github.com/gofiber/fiber/v2 v2.52.14
github.com/gofiber/swagger v1.1.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/iancoleman/strcase v0.3.0
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/jeremywohl/flatten v1.0.1
@@ -84,7 +84,6 @@ require (
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
-2
View File
@@ -184,8 +184,6 @@ github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA
github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+18 -15
View File
@@ -1,7 +1,6 @@
package common
import (
"encoding/json"
"fmt"
"strings"
"time"
@@ -9,23 +8,23 @@ import (
"github.com/artalkjs/artalk/v2/internal/core"
"github.com/artalkjs/artalk/v2/internal/entity"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
"github.com/golang-jwt/jwt/v5"
)
// jwtCustomClaims are custom claims extending default ones.
// See https://github.com/golang-jwt/jwt for more examples
type jwtCustomClaims struct {
UserID uint `json:"user_id"`
jwt.StandardClaims
jwt.RegisteredClaims
}
func LoginGetUserToken(user entity.User, key string, ttl int) (string, error) {
// Set custom claims
claims := &jwtCustomClaims{
UserID: user.ID,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(), // 签发时间
ExpiresAt: time.Now().Add(time.Second * time.Duration(ttl)).Unix(), // 过期时间
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(ttl))), // 过期时间
},
}
@@ -62,22 +61,22 @@ func GetJwtDataByReq(app *core.App, c *fiber.Ctx) (jwtCustomClaims, error) {
if token == "" {
return jwtCustomClaims{}, ErrTokenNotProvided
}
return parseJWTToken(token, app.Conf().AppKey)
}
jwt, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != "HS256" {
func parseJWTToken(token, key string) (jwtCustomClaims, error) {
claims := jwtCustomClaims{}
_, err := jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
}
return []byte(app.Conf().AppKey), nil // 密钥
})
return []byte(key), nil // 密钥
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return jwtCustomClaims{}, err
}
claims := jwtCustomClaims{}
tmp, _ := json.Marshal(jwt.Claims)
_ = json.Unmarshal(tmp, &claims)
return claims, nil
}
@@ -93,7 +92,11 @@ func GetUserByReq(app *core.App, c *fiber.Ctx) (entity.User, error) {
}
// check tokenValidFrom
if user.TokenValidFrom.Valid && claims.IssuedAt < user.TokenValidFrom.Time.Unix() {
issuedAt := int64(0)
if claims.IssuedAt != nil {
issuedAt = claims.IssuedAt.Unix()
}
if user.TokenValidFrom.Valid && issuedAt < user.TokenValidFrom.Time.Unix() {
return entity.User{}, ErrTokenInvalidFromDate
}
+70
View File
@@ -0,0 +1,70 @@
package common
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"testing"
"time"
"github.com/artalkjs/artalk/v2/internal/entity"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoginGetUserToken(t *testing.T) {
const key = "test-secret"
user := entity.User{}
user.ID = 42
token, err := LoginGetUserToken(user, key, 60)
require.NoError(t, err)
claims, err := parseJWTToken(token, key)
require.NoError(t, err)
assert.Equal(t, uint(42), claims.UserID)
assert.WithinDuration(t, time.Now(), claims.IssuedAt.Time, time.Second)
assert.WithinDuration(t, time.Now().Add(time.Minute), claims.ExpiresAt.Time, time.Second)
}
func TestParseJWTTokenAcceptsLegacyClaims(t *testing.T) {
const key = "test-secret"
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"user_id":42,"exp":4102444800,"iat":1600000000}`))
unsigned := header + "." + payload
mac := hmac.New(sha256.New, []byte(key))
_, _ = mac.Write([]byte(unsigned))
token := unsigned + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
claims, err := parseJWTToken(token, key)
require.NoError(t, err)
assert.Equal(t, uint(42), claims.UserID)
assert.Equal(t, int64(1600000000), claims.IssuedAt.Unix())
}
func TestParseJWTTokenRejectsUnexpectedAlgorithm(t *testing.T) {
const key = "test-secret"
token, err := jwt.NewWithClaims(jwt.SigningMethodHS384, jwt.MapClaims{
"user_id": 42,
"exp": time.Now().Add(time.Minute).Unix(),
}).SignedString([]byte(key))
require.NoError(t, err)
_, err = parseJWTToken(token, key)
require.Error(t, err)
assert.Contains(t, err.Error(), fmt.Sprintf("signing method %s is invalid", jwt.SigningMethodHS384.Alg()))
}
func TestParseJWTTokenRejectsExpiredToken(t *testing.T) {
const key = "test-secret"
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": 42,
"exp": time.Now().Add(-time.Minute).Unix(),
}).SignedString([]byte(key))
require.NoError(t, err)
_, err = parseJWTToken(token, key)
require.Error(t, err)
assert.ErrorIs(t, err, jwt.ErrTokenExpired)
}