b 5 luni în urmă
părinte
comite
a5bf205be9

+ 14 - 0
gin/jwtx/go.mod

@@ -0,0 +1,14 @@
+module github.com/5-say/go-kit/gin/jwtx
+
+go 1.21
+
+require (
+	github.com/golang-jwt/jwt/v5 v5.3.1
+	github.com/stretchr/testify v1.8.3
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 12 - 0
gin/jwtx/go.sum

@@ -0,0 +1,12 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 264 - 0
gin/jwtx/test/Tool_test.go

@@ -0,0 +1,264 @@
+package test
+
+import (
+	"crypto/ed25519"
+	"encoding/pem"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/5-say/go-kit/gin/jwtx/tool"
+	"github.com/golang-jwt/jwt/v5"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestGeneratePrivateKey 测试 GeneratePrivateKey 函数
+func TestGeneratePrivateKey(t *testing.T) {
+	t.Run("生成有效的私钥", func(t *testing.T) {
+		privateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+		assert.NotNil(t, privateKey)
+		assert.Equal(t, ed25519.PrivateKeySize, len(*privateKey))
+	})
+
+	t.Run("生成的私钥可以用于签名", func(t *testing.T) {
+		privateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+
+		// 测试私钥可以用于签名
+		message := []byte("test message")
+		signature := ed25519.Sign(*privateKey, message)
+
+		// 验证签名
+		publicKey := privateKey.Public().(ed25519.PublicKey)
+		isValid := ed25519.Verify(publicKey, message, signature)
+		assert.True(t, isValid)
+	})
+}
+
+// TestGenerateToken 测试 GenerateToken 函数
+func TestGenerateToken(t *testing.T) {
+	// 生成测试用的私钥
+	privateKey, err := tool.GeneratePrivateKey()
+	require.NoError(t, err)
+
+	t.Run("生成有效的 JWT token", func(t *testing.T) {
+		claims := jwt.MapClaims{
+			"user_id": "12345",
+			"role":    "admin",
+			"iss":     "test-issuer",
+			"exp":     time.Now().Add(time.Hour).Unix(),
+			"iat":     time.Now().Unix(),
+		}
+
+		token, err := tool.GenerateToken(privateKey, claims)
+		require.NoError(t, err)
+		assert.NotEmpty(t, token)
+		assert.Contains(t, token, ".") // JWT token 应该包含点分隔符
+	})
+
+	t.Run("生成的 token 可以被正确解析", func(t *testing.T) {
+		claims := jwt.MapClaims{
+			"user_id": "67890",
+			"email":   "test@example.com",
+			"exp":     time.Now().Add(time.Hour).Unix(),
+		}
+
+		token, err := tool.GenerateToken(privateKey, claims)
+		require.NoError(t, err)
+
+		// 使用公钥解析 token
+		publicKey := privateKey.Public().(ed25519.PublicKey)
+		parsedClaims, err := tool.ParseToken(token, &publicKey)
+		require.NoError(t, err)
+
+		assert.Equal(t, "67890", parsedClaims["user_id"])
+		assert.Equal(t, "test@example.com", parsedClaims["email"])
+	})
+
+	t.Run("空声明生成 token", func(t *testing.T) {
+		claims := jwt.MapClaims{}
+
+		token, err := tool.GenerateToken(privateKey, claims)
+		require.NoError(t, err)
+		assert.NotEmpty(t, token)
+	})
+}
+
+// TestParseToken 测试 ParseToken 函数
+func TestParseToken(t *testing.T) {
+	// 生成测试用的密钥对
+	privateKey, err := tool.GeneratePrivateKey()
+	require.NoError(t, err)
+	publicKey := privateKey.Public().(ed25519.PublicKey)
+
+	t.Run("解析有效的 token", func(t *testing.T) {
+		originalClaims := jwt.MapClaims{
+			"user_id": "test-user",
+			"scope":   "read write",
+			"exp":     time.Now().Add(time.Hour).Unix(),
+		}
+
+		token, err := tool.GenerateToken(privateKey, originalClaims)
+		require.NoError(t, err)
+
+		parsedClaims, err := tool.ParseToken(token, &publicKey)
+		require.NoError(t, err)
+
+		assert.Equal(t, originalClaims["user_id"], parsedClaims["user_id"])
+		assert.Equal(t, originalClaims["scope"], parsedClaims["scope"])
+	})
+
+	t.Run("解析无效的 token 应该失败", func(t *testing.T) {
+		invalidToken := "invalid.jwt.token"
+
+		_, err := tool.ParseToken(invalidToken, &publicKey)
+		assert.Error(t, err)
+	})
+
+	t.Run("使用错误的公钥解析应该失败", func(t *testing.T) {
+		// 生成另一个密钥对
+		wrongPrivateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+		wrongPublicKey := wrongPrivateKey.Public().(ed25519.PublicKey)
+
+		claims := jwt.MapClaims{"test": "value"}
+		token, err := tool.GenerateToken(privateKey, claims)
+		require.NoError(t, err)
+
+		// 使用错误的公钥解析
+		_, err = tool.ParseToken(token, &wrongPublicKey)
+		assert.Error(t, err)
+	})
+
+	t.Run("解析过期的 token 应该失败", func(t *testing.T) {
+		// 创建过期的 claims
+		expiredClaims := jwt.MapClaims{
+			"user_id": "expired-user",
+			"exp":     time.Now().Add(-time.Hour).Unix(), // 过去的时间
+		}
+
+		token, err := tool.GenerateToken(privateKey, expiredClaims)
+		require.NoError(t, err)
+
+		_, err = tool.ParseToken(token, &publicKey)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "expired")
+	})
+}
+
+// TestGetPrivateKey 测试 GetPrivateKey 函数
+func TestGetPrivateKey(t *testing.T) {
+	// 创建临时文件用于测试
+	tempFile, err := os.CreateTemp("", "test_private_key_*.pem")
+	require.NoError(t, err)
+	defer os.Remove(tempFile.Name())
+
+	t.Run("从有效的 PEM 文件加载私钥", func(t *testing.T) {
+		// 生成私钥并保存到文件
+		privateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+
+		// 将私钥保存为 PEM 格式
+		pemData := pemEncodePrivateKey(*privateKey)
+		err = os.WriteFile(tempFile.Name(), pemData, 0600)
+		require.NoError(t, err)
+
+		// 从文件加载私钥
+		loadedKey, err := tool.GetPrivateKey(tempFile.Name())
+		require.NoError(t, err)
+		assert.NotNil(t, loadedKey)
+		assert.Equal(t, *privateKey, *loadedKey)
+	})
+
+	t.Run("加载不存在的文件应该失败", func(t *testing.T) {
+		_, err := tool.GetPrivateKey("/nonexistent/file.pem")
+		assert.Error(t, err)
+	})
+
+	t.Run("加载无效的 PEM 文件应该失败", func(t *testing.T) {
+		// 写入无效的 PEM 数据
+		err := os.WriteFile(tempFile.Name(), []byte("invalid pem data"), 0600)
+		require.NoError(t, err)
+
+		_, err = tool.GetPrivateKey(tempFile.Name())
+		assert.Error(t, err)
+	})
+
+	t.Run("加载非私钥的 PEM 文件应该失败", func(t *testing.T) {
+		// 写入公钥 PEM 数据(应该失败)
+		privateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+		publicKey := privateKey.Public().(ed25519.PublicKey)
+
+		pemData := pemEncodePublicKey(publicKey)
+		err = os.WriteFile(tempFile.Name(), pemData, 0600)
+		require.NoError(t, err)
+
+		_, err = tool.GetPrivateKey(tempFile.Name())
+		assert.Error(t, err)
+	})
+}
+
+// TestIntegration 集成测试:完整的 JWT 流程
+func TestIntegration(t *testing.T) {
+	t.Run("完整的 JWT 生成和验证流程", func(t *testing.T) {
+		// 1. 生成私钥
+		privateKey, err := tool.GeneratePrivateKey()
+		require.NoError(t, err)
+
+		// 2. 创建 claims
+		claims := jwt.MapClaims{
+			"user_id": "integration-test",
+			"name":    "Test User",
+			"email":   "test@integration.com",
+			"roles":   []string{"user", "admin"},
+			"exp":     time.Now().Add(2 * time.Hour).Unix(),
+			"iat":     time.Now().Unix(),
+			"iss":     "integration-test-server",
+		}
+
+		// 3. 生成 JWT token
+		token, err := tool.GenerateToken(privateKey, claims)
+		require.NoError(t, err)
+		assert.NotEmpty(t, token)
+
+		// 4. 获取公钥
+		publicKey := privateKey.Public().(ed25519.PublicKey)
+
+		// 5. 解析和验证 token
+		parsedClaims, err := tool.ParseToken(token, &publicKey)
+		require.NoError(t, err)
+
+		// 6. 验证 claims 内容
+		assert.Equal(t, claims["user_id"], parsedClaims["user_id"])
+		assert.Equal(t, claims["name"], parsedClaims["name"])
+		assert.Equal(t, claims["email"], parsedClaims["email"])
+		assert.Equal(t, claims["iss"], parsedClaims["iss"])
+
+		// 7. 验证 token 可以用于实际场景
+		// 例如:在中间件中验证 token 的有效性
+		_, err = tool.ParseToken(token, &publicKey)
+		assert.NoError(t, err, "token 应该可以重复验证")
+	})
+}
+
+// pemEncodePrivateKey 将 Ed25519 私钥编码为 PEM 格式
+func pemEncodePrivateKey(privateKey ed25519.PrivateKey) []byte {
+	// 注意:ed25519.PrivateKey 实际上是 []byte,可以直接作为 PEM 数据
+	block := &pem.Block{
+		Type:  "PRIVATE KEY",
+		Bytes: privateKey,
+	}
+	return pem.EncodeToMemory(block)
+}
+
+// pemEncodePublicKey 将 Ed25519 公钥编码为 PEM 格式
+func pemEncodePublicKey(publicKey ed25519.PublicKey) []byte {
+	block := &pem.Block{
+		Type:  "PUBLIC KEY",
+		Bytes: publicKey,
+	}
+	return pem.EncodeToMemory(block)
+}

+ 46 - 0
gin/jwtx/tool/GeneratePrivateKey.go

@@ -0,0 +1,46 @@
+package tool
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"fmt"
+)
+
+// GeneratePrivateKey 生成一个新的 Ed25519 私钥
+//
+// 返回值:
+//   - privateKey: 生成的 Ed25519 私钥指针
+//   - err: 错误信息,如果生成失败则返回错误
+//
+// 示例:
+//
+//	privateKey, err := GeneratePrivateKey()
+//	if err != nil {
+//		// 处理错误
+//	}
+//	// 使用私钥生成 JWT token
+//	token, err := GenerateToken(privateKey, claims)
+func GeneratePrivateKey() (privateKey *ed25519.PrivateKey, err error) {
+	// 使用 crypto/rand 生成安全的随机数作为种子
+	// Ed25519 私钥实际上是 32 字节的种子,公钥由种子推导而来
+	var seed [32]byte
+
+	// 从加密安全的随机数生成器读取种子
+	_, err = rand.Read(seed[:])
+	if err != nil {
+		return nil, fmt.Errorf("生成随机种子失败: %w", err)
+	}
+
+	// 使用种子生成 Ed25519 密钥对
+	// ed25519.NewKeyFromSeed 返回的是 ed25519.PrivateKey 类型(实际上是 []byte)
+	privKey := ed25519.NewKeyFromSeed(seed[:])
+
+	// 验证生成的私钥长度是否正确
+	if len(privKey) != ed25519.PrivateKeySize {
+		return nil, fmt.Errorf("生成的私钥长度无效: 期望 %d 字节,实际 %d 字节",
+			ed25519.PrivateKeySize, len(privKey))
+	}
+
+	// 返回私钥的指针
+	return &privKey, nil
+}

+ 54 - 0
gin/jwtx/tool/GenerateToken.go

@@ -0,0 +1,54 @@
+package tool
+
+import (
+	"crypto/ed25519"
+
+	"github.com/golang-jwt/jwt/v5"
+)
+
+// GenerateToken 生成 JWT token,仅支持 *ed25519.PrivateKey 私钥类型
+//
+// 参数:
+//   - key: Ed25519 私钥,必须为 *ed25519.PrivateKey 类型
+//   - claims: JWT 声明内容,包含用户自定义的数据
+//
+// 返回值:
+//   - token: 生成的 JWT token 字符串
+//   - err: 错误信息,如果生成失败则返回错误
+//
+// 示例:
+//
+//	// 使用指向 Ed25519 私钥的指针
+//	token, err := GenerateToken(&ed25519PrivateKey, claims)
+func GenerateToken(key *ed25519.PrivateKey, claims jwt.MapClaims) (token string, err error) {
+	// 直接使用 EdDSA 签名方法生成 token
+	return jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims).SignedString(key)
+}
+
+// JWT Claims (声明) 是 JWT token 中包含的数据部分
+//
+// JWT Claims 包含三部分内容:
+// 1. 注册声明 (Registered Claims): 标准化的声明字段,包括:
+//    - iss (issuer):          签发者
+//    - sub (subject):         主题
+//    - aud (audience):        受众
+//    - exp (expiration time): 过期时间
+//    - nbf (not before):      生效时间
+//    - iat (issued at):       签发时间
+//    - jti (JWT ID):          JWT 唯一标识符
+// 2. 公共声明 (Public Claims):    预定义的公共声明字段
+// 3. 私有声明 (Private Claims):   用户自定义的声明字段
+//
+// 在 GenerateToken 函数中,claims 参数用于设置 JWT token 的声明内容
+// 示例:
+//   claims := jwt.MapClaims{
+//       "user_id": "12345",                               // 私有声明
+//       "role":    "admin",                               // 私有声明
+//       "iss":     "my-app",                              // 注册声明 - 签发者
+//       "sub":     "user-auth",                           // 注册声明 - 主题
+//       "aud":     "api-service",                         // 注册声明 - 受众
+//       "exp":     time.Now().Add(24 * time.Hour).Unix(), // 注册声明 - 过期时间
+//       "nbf":     time.Now().Unix(),                     // 注册声明 - 生效时间
+//       "iat":     time.Now().Unix(),                     // 注册声明 - 签发时间
+//       "jti":     "unique-token-id",                     // 注册声明 - JWT ID
+//   }

+ 61 - 0
gin/jwtx/tool/GetPrivateKey.go

@@ -0,0 +1,61 @@
+package tool
+
+import (
+	"crypto/ed25519"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"os"
+)
+
+// GetPrivateKey 从 PEM 文件中加载 Ed25519 私钥
+//
+// 参数:
+//   - filePath: PEM 格式私钥文件的路径
+//
+// 返回值:
+//   - privateKey: 加载的 Ed25519 私钥指针
+//   - err: 错误信息,如果加载失败则返回错误
+//
+// 示例:
+//
+//	privateKey, err := GetPrivateKey("private.pem")
+//	if err != nil {
+//		// 处理错误
+//	}
+func GetPrivateKey(filePath string) (privateKey *ed25519.PrivateKey, err error) {
+	// 读取文件内容
+	data, err := os.ReadFile(filePath)
+	if err != nil {
+		return nil, fmt.Errorf("读取私钥文件失败: %w", err)
+	}
+
+	// 解析 PEM 块
+	block, _ := pem.Decode(data)
+	if block == nil {
+		return nil, errors.New("无法解析 PEM 块")
+	}
+
+	// 检查 PEM 块类型
+	if block.Type != "PRIVATE KEY" && block.Type != "ED25519 PRIVATE KEY" {
+		return nil, fmt.Errorf("不支持的 PEM 块类型: %s,期望 PRIVATE KEY 或 ED25519 PRIVATE KEY", block.Type)
+	}
+
+	// 从 PEM 块数据中解析私钥
+	// 注意:ed25519.PrivateKey 实际上是一个字节切片,不是结构体
+	// 所以我们需要直接使用字节数据
+	if len(block.Bytes) != ed25519.PrivateKeySize {
+		return nil, fmt.Errorf("私钥长度无效: 期望 %d 字节,实际 %d 字节", ed25519.PrivateKeySize, len(block.Bytes))
+	}
+
+	// 创建私钥(注意:ed25519.PrivateKey 是 []byte 类型)
+	privKey := ed25519.PrivateKey(block.Bytes)
+
+	// 验证私钥是否有效
+	if len(privKey) != ed25519.PrivateKeySize {
+		return nil, errors.New("私钥长度验证失败")
+	}
+
+	// 返回私钥的指针
+	return &privKey, nil
+}

+ 55 - 0
gin/jwtx/tool/ParseToken.go

@@ -0,0 +1,55 @@
+package tool
+
+import (
+	"crypto/ed25519"
+	"errors"
+
+	"github.com/golang-jwt/jwt/v5"
+)
+
+// ParseToken 解析和验证 JWT token,使用 Ed25519 公钥进行签名验证
+//
+// 参数:
+//   - tokenStr: 要解析的 JWT token 字符串
+//   - publicKey: Ed25519 公钥,用于验证 token 签名
+//
+// 返回值:
+//   - claims: 解析后的 JWT 声明内容
+//   - err: 错误信息,如果解析或验证失败则返回错误
+//
+// 示例:
+//
+//	claims, err := ParseToken(tokenString, publicKey)
+//	if err != nil {
+//		// 处理错误
+//	}
+//	userID := claims["user_id"].(string)
+func ParseToken(tokenStr string, publicKey *ed25519.PublicKey) (claims jwt.MapClaims, err error) {
+	// 使用 jwt.Parse 解析 token,并验证签名
+	token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
+		// 验证签名算法是否为 EdDSA
+		if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
+			return nil, errors.New("不支持的签名算法,期望 EdDSA")
+		}
+
+		// 返回公钥用于签名验证
+		return *publicKey, nil
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	// 检查 token 是否有效
+	if !token.Valid {
+		return nil, errors.New("无效的 token")
+	}
+
+	// 提取声明内容
+	claims, ok := token.Claims.(jwt.MapClaims)
+	if !ok {
+		return nil, errors.New("无法解析声明内容")
+	}
+
+	return claims, nil
+}