qdy 3 недель назад
Родитель
Сommit
eb3c2c6203

+ 214
- 0
cmd/demo-login/main.go Просмотреть файл

1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"time"
11
+
12
+	"git.x2erp.com/qdy/go-base/sdk/configure"
13
+)
14
+
15
+func main() {
16
+	fmt.Println("=== 演示登录功能测试 ===")
17
+
18
+	// 配置服务地址
19
+	configureURL := "http://localhost:8080"
20
+	svcCodeURL := "http://localhost:8020"
21
+
22
+	fmt.Printf("配置中心地址: %s\n", configureURL)
23
+	fmt.Printf("svc-code地址: %s\n", svcCodeURL)
24
+
25
+	// 测试凭证
26
+	userID := "test-user-001"
27
+	password := "password123"
28
+
29
+	fmt.Printf("\n使用测试用户: %s\n", userID)
30
+
31
+	// 1. 直接使用SDK登录配置中心
32
+	fmt.Println("\n--- 方法1: 直接使用SDK登录配置中心 ---")
33
+	token1, err := loginWithSDK(configureURL, userID, password)
34
+	if err != nil {
35
+		fmt.Printf("SDK登录失败: %v\n", err)
36
+	} else {
37
+		fmt.Printf("✅ SDK登录成功!\n")
38
+		fmt.Printf("Token (前100字符): %s...\n", token1[:min(100, len(token1))])
39
+		fmt.Printf("Token长度: %d 字符\n", len(token1))
40
+
41
+		// 验证token
42
+		if err := validateTokenWithSDK(configureURL, token1); err != nil {
43
+			fmt.Printf("Token验证失败: %v\n", err)
44
+		} else {
45
+			fmt.Printf("✅ Token验证通过\n")
46
+		}
47
+	}
48
+
49
+	// 2. 通过svc-code API登录
50
+	fmt.Println("\n--- 方法2: 通过svc-code API登录 ---")
51
+	token2, err := loginWithAPI(svcCodeURL, userID, password)
52
+	if err != nil {
53
+		fmt.Printf("API登录失败: %v\n", err)
54
+	} else {
55
+		fmt.Printf("✅ API登录成功!\n")
56
+		fmt.Printf("Token (前100字符): %s...\n", token2[:min(100, len(token2))])
57
+		fmt.Printf("Token长度: %d 字符\n", len(token2))
58
+
59
+		// 比较两个token是否相同
60
+		if token1 != "" && token2 != "" {
61
+			if token1 == token2 {
62
+				fmt.Printf("✅ 两个方法返回的Token相同\n")
63
+			} else {
64
+				fmt.Printf("⚠️  两个方法返回的Token不同(可能是不同的JWT有效期)\n")
65
+			}
66
+		}
67
+	}
68
+
69
+	// 3. 测试无效凭证
70
+	fmt.Println("\n--- 测试无效凭证 ---")
71
+	testInvalidCredentials(svcCodeURL)
72
+
73
+	fmt.Println("\n=== 测试完成 ===")
74
+}
75
+
76
+// loginWithSDK 使用SDK直接登录配置中心
77
+func loginWithSDK(configureURL, userID, password string) (string, error) {
78
+	// 创建SDK客户端
79
+	client, err := configure.NewBasicAuthClient(configureURL, "test", "test")
80
+	if err != nil {
81
+		return "", fmt.Errorf("创建SDK客户端失败: %w", err)
82
+	}
83
+
84
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
85
+	defer cancel()
86
+
87
+	req := &configure.UserLoginRequest{
88
+		UserID:   userID,
89
+		Password: password,
90
+	}
91
+
92
+	startTime := time.Now()
93
+	token, err := client.LoginUser(ctx, req)
94
+	elapsed := time.Since(startTime)
95
+
96
+	if err != nil {
97
+		return "", fmt.Errorf("登录失败: %w (耗时: %v)", err, elapsed)
98
+	}
99
+
100
+	fmt.Printf("登录耗时: %v\n", elapsed)
101
+	return token, nil
102
+}
103
+
104
+// validateTokenWithSDK 使用SDK验证token
105
+func validateTokenWithSDK(configureURL, token string) error {
106
+	// 使用token创建配置中心客户端
107
+	client, err := configure.NewTokenAuthClient(configureURL, token)
108
+	if err != nil {
109
+		return fmt.Errorf("创建Token客户端失败: %w", err)
110
+	}
111
+
112
+	fmt.Printf("Token客户端创建成功,BaseURL: %s\n", client.GetConfig().BaseURL)
113
+	return nil
114
+}
115
+
116
+// loginWithAPI 通过svc-code API登录
117
+func loginWithAPI(svcCodeURL, userID, password string) (string, error) {
118
+	client := &http.Client{
119
+		Timeout: 30 * time.Second,
120
+	}
121
+
122
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
123
+	loginData := map[string]string{
124
+		"user_id":  userID,
125
+		"password": password,
126
+	}
127
+
128
+	loginJSON, err := json.Marshal(loginData)
129
+	if err != nil {
130
+		return "", fmt.Errorf("JSON序列化失败: %w", err)
131
+	}
132
+
133
+	startTime := time.Now()
134
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(loginJSON))
135
+	if err != nil {
136
+		return "", fmt.Errorf("创建HTTP请求失败: %w", err)
137
+	}
138
+	req.Header.Set("Content-Type", "application/json")
139
+
140
+	resp, err := client.Do(req)
141
+	if err != nil {
142
+		return "", fmt.Errorf("发送登录请求失败: %w", err)
143
+	}
144
+	defer resp.Body.Close()
145
+
146
+	elapsed := time.Since(startTime)
147
+	fmt.Printf("API调用耗时: %v\n", elapsed)
148
+
149
+	if resp.StatusCode != 200 {
150
+		body, _ := io.ReadAll(resp.Body)
151
+		return "", fmt.Errorf("API返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
152
+	}
153
+
154
+	body, err := io.ReadAll(resp.Body)
155
+	if err != nil {
156
+		return "", fmt.Errorf("读取响应体失败: %w", err)
157
+	}
158
+
159
+	var result map[string]interface{}
160
+	if err := json.Unmarshal(body, &result); err != nil {
161
+		return "", fmt.Errorf("解析响应JSON失败: %w, 响应体: %s", err, string(body))
162
+	}
163
+
164
+	if !result["success"].(bool) {
165
+		return "", fmt.Errorf("登录失败: %v", result)
166
+	}
167
+
168
+	token, ok := result["data"].(string)
169
+	if !ok || token == "" {
170
+		return "", fmt.Errorf("响应中未找到有效token: %v", result)
171
+	}
172
+
173
+	return token, nil
174
+}
175
+
176
+// testInvalidCredentials 测试无效凭证
177
+func testInvalidCredentials(svcCodeURL string) {
178
+	client := &http.Client{
179
+		Timeout: 10 * time.Second,
180
+	}
181
+
182
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
183
+	invalidData := map[string]string{
184
+		"user_id":  "invalid-user",
185
+		"password": "wrong-password",
186
+	}
187
+
188
+	invalidJSON, _ := json.Marshal(invalidData)
189
+
190
+	resp, err := client.Post(loginURL, "application/json", bytes.NewReader(invalidJSON))
191
+	if err != nil {
192
+		fmt.Printf("无效凭证测试失败: %v\n", err)
193
+		return
194
+	}
195
+	defer resp.Body.Close()
196
+
197
+	body, _ := io.ReadAll(resp.Body)
198
+	var result map[string]interface{}
199
+	json.Unmarshal(body, &result)
200
+
201
+	if !result["success"].(bool) {
202
+		fmt.Printf("✅ 无效凭证登录失败(预期): %s\n", result["message"])
203
+	} else {
204
+		fmt.Printf("❌ 无效凭证登录不应该成功\n")
205
+	}
206
+}
207
+
208
+// min 返回两个整数的最小值
209
+func min(a, b int) int {
210
+	if a < b {
211
+		return a
212
+	}
213
+	return b
214
+}

+ 64
- 0
internal/routes/auth_routes.go Просмотреть файл

1
+package routes
2
+
3
+import (
4
+	"context"
5
+
6
+	"git.x2erp.com/qdy/go-base/authbase"
7
+	"git.x2erp.com/qdy/go-base/ctx"
8
+	"git.x2erp.com/qdy/go-base/model/response"
9
+	"git.x2erp.com/qdy/go-base/sdk/configure"
10
+	"git.x2erp.com/qdy/go-base/webx/router"
11
+)
12
+
13
+// RegisterAuthRoutes 注册认证路由
14
+func RegisterAuthRoutes(ws *router.RouterService, configClient *configure.Client) {
15
+	// 用户登录(公开端点,无需认证)
16
+	ws.POST("/api/auth/login",
17
+		func(req *configure.UserLoginRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[string], error) {
18
+			token, err := configClient.LoginUser(ctx, req)
19
+			if err != nil {
20
+				return &response.QueryResult[string]{
21
+					Success: false,
22
+					Message: err.Error(),
23
+				}, nil
24
+			}
25
+
26
+			return &response.QueryResult[string]{
27
+				Success: true,
28
+				Data:    token,
29
+			}, nil
30
+		},
31
+	).Desc("用户登录(返回配置中心token)").Register()
32
+
33
+	// Token验证端点(需要Token认证,用于测试token有效性)
34
+	ws.POST("/api/auth/validate",
35
+		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
36
+			// 如果请求能到达这里,说明TokenAuth中间件已经验证了token
37
+			// 返回当前用户信息
38
+			userInfo := map[string]interface{}{
39
+				"user_id":       reqCtx.UserID,
40
+				"username":      reqCtx.Username,
41
+				"tenant_id":     reqCtx.TenantID,
42
+				"project_id":    reqCtx.ProjectID,
43
+				"authenticated": true,
44
+			}
45
+			return &response.QueryResult[map[string]interface{}]{
46
+				Success: true,
47
+				Data:    userInfo,
48
+			}, nil
49
+		},
50
+	).Use(authbase.TokenAuth).Desc("验证token有效性并返回用户信息").Register()
51
+
52
+	// 健康检查端点(公开)
53
+	ws.GET("/api/auth/health",
54
+		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
55
+			return &response.QueryResult[map[string]interface{}]{
56
+				Success: true,
57
+				Data: map[string]interface{}{
58
+					"status":  "healthy",
59
+					"service": "svc-code-auth",
60
+				},
61
+			}, nil
62
+		},
63
+	).Desc("认证服务健康检查").Register()
64
+}

+ 7
- 2
internal/routes/log_stream_routes.go Просмотреть файл

5
 	"net/http"
5
 	"net/http"
6
 	"time"
6
 	"time"
7
 
7
 
8
+	"git.x2erp.com/qdy/go-base/authbase"
8
 	"git.x2erp.com/qdy/go-base/webx/router"
9
 	"git.x2erp.com/qdy/go-base/webx/router"
9
 	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
10
 	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
10
 )
11
 )
17
 	// 这个函数将由 main.go 中的额外注册调用
18
 	// 这个函数将由 main.go 中的额外注册调用
18
 }
19
 }
19
 
20
 
20
-// LogStreamHandler 日志流的 HTTP 处理器
21
+// LogStreamHandler 日志流的 HTTP 处理器(已包含TokenAuth认证)
21
 func LogStreamHandler(process *opencode.Process) http.HandlerFunc {
22
 func LogStreamHandler(process *opencode.Process) http.HandlerFunc {
22
-	return func(w http.ResponseWriter, r *http.Request) {
23
+	// 创建内部处理器
24
+	handler := func(w http.ResponseWriter, r *http.Request) {
23
 		// 设置 SSE 头
25
 		// 设置 SSE 头
24
 		w.Header().Set("Content-Type", "text/event-stream")
26
 		w.Header().Set("Content-Type", "text/event-stream")
25
 		w.Header().Set("Cache-Control", "no-cache")
27
 		w.Header().Set("Cache-Control", "no-cache")
66
 			}
68
 			}
67
 		}
69
 		}
68
 	}
70
 	}
71
+
72
+	// 包装TokenAuth中间件
73
+	return authbase.TokenAuth(http.HandlerFunc(handler)).ServeHTTP
69
 }
74
 }

+ 9
- 4
internal/routes/prompt_stream_routes.go Просмотреть файл

8
 	"net/http"
8
 	"net/http"
9
 	"time"
9
 	"time"
10
 
10
 
11
+	"git.x2erp.com/qdy/go-base/authbase"
11
 	"git.x2erp.com/qdy/go-base/ctx"
12
 	"git.x2erp.com/qdy/go-base/ctx"
12
 	"git.x2erp.com/qdy/go-base/model/response"
13
 	"git.x2erp.com/qdy/go-base/model/response"
13
 	"git.x2erp.com/qdy/go-base/webx/router"
14
 	"git.x2erp.com/qdy/go-base/webx/router"
24
 
25
 
25
 // RegisterPromptStreamRoutes 注册流式对话路由
26
 // RegisterPromptStreamRoutes 注册流式对话路由
26
 func RegisterPromptStreamRoutes(ws *router.RouterService, client *opencode.Client) {
27
 func RegisterPromptStreamRoutes(ws *router.RouterService, client *opencode.Client) {
27
-	// 流式对话
28
+	// 流式对话(需要Token认证)
28
 	ws.POST("/api/prompt/stream",
29
 	ws.POST("/api/prompt/stream",
29
 		func(req *PromptStreamRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
30
 		func(req *PromptStreamRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
30
 			// 流式响应需要直接写入 HTTP 响应,不能使用标准的路由返回值
31
 			// 流式响应需要直接写入 HTTP 响应,不能使用标准的路由返回值
34
 				Message: "流式端点需要特殊处理",
35
 				Message: "流式端点需要特殊处理",
35
 			}, nil
36
 			}, nil
36
 		},
37
 		},
37
-	).Desc("流式对话(Server-Sent Events)").Register()
38
+	).Use(authbase.TokenAuth).Desc("流式对话(Server-Sent Events)").Register()
38
 
39
 
39
 	// 流式对话的原始 HTTP 处理器
40
 	// 流式对话的原始 HTTP 处理器
40
 	// 注意:这个路由需要直接处理 HTTP 流,不能使用标准的 router 包装
41
 	// 注意:这个路由需要直接处理 HTTP 流,不能使用标准的 router 包装
42
 	// 这个函数将由 main.go 中的额外注册调用
43
 	// 这个函数将由 main.go 中的额外注册调用
43
 }
44
 }
44
 
45
 
45
-// StreamPromptHandler 流式对话的 HTTP 处理器
46
+// StreamPromptHandler 流式对话的 HTTP 处理器(已包含TokenAuth认证)
46
 func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
47
 func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
47
-	return func(w http.ResponseWriter, r *http.Request) {
48
+	// 创建内部处理器
49
+	handler := func(w http.ResponseWriter, r *http.Request) {
48
 		// 解析请求
50
 		// 解析请求
49
 		var req PromptStreamRequest
51
 		var req PromptStreamRequest
50
 		if err := BindJSON(r, &req); err != nil {
52
 		if err := BindJSON(r, &req); err != nil {
102
 			}
104
 			}
103
 		}
105
 		}
104
 	}
106
 	}
107
+
108
+	// 包装TokenAuth中间件
109
+	return authbase.TokenAuth(http.HandlerFunc(handler)).ServeHTTP
105
 }
110
 }
106
 
111
 
107
 // BindJSON 简单的 JSON 绑定函数
112
 // BindJSON 简单的 JSON 绑定函数

+ 3
- 2
internal/routes/prompt_sync_routes.go Просмотреть файл

3
 import (
3
 import (
4
 	"context"
4
 	"context"
5
 
5
 
6
+	"git.x2erp.com/qdy/go-base/authbase"
6
 	"git.x2erp.com/qdy/go-base/ctx"
7
 	"git.x2erp.com/qdy/go-base/ctx"
7
 	"git.x2erp.com/qdy/go-base/model/response"
8
 	"git.x2erp.com/qdy/go-base/model/response"
8
 	"git.x2erp.com/qdy/go-base/webx/router"
9
 	"git.x2erp.com/qdy/go-base/webx/router"
25
 
26
 
26
 // RegisterPromptSyncRoutes 注册同步对话路由
27
 // RegisterPromptSyncRoutes 注册同步对话路由
27
 func RegisterPromptSyncRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
28
 func RegisterPromptSyncRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
28
-	// 同步对话
29
+	// 同步对话(需要Token认证)
29
 	ws.POST("/api/prompt/sync",
30
 	ws.POST("/api/prompt/sync",
30
 		func(req *PromptSyncRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[PromptSyncResponse], error) {
31
 		func(req *PromptSyncRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[PromptSyncResponse], error) {
31
 			prompt := &opencode.PromptRequest{
32
 			prompt := &opencode.PromptRequest{
50
 				},
51
 				},
51
 			}, nil
52
 			}, nil
52
 		},
53
 		},
53
-	).Desc("同步对话(阻塞等待完整响应)").Register()
54
+	).Use(authbase.TokenAuth).Desc("同步对话(阻塞等待完整响应)").Register()
54
 }
55
 }

+ 5
- 4
internal/routes/session_routes.go Просмотреть файл

3
 import (
3
 import (
4
 	"context"
4
 	"context"
5
 
5
 
6
+	"git.x2erp.com/qdy/go-base/authbase"
6
 	"git.x2erp.com/qdy/go-base/ctx"
7
 	"git.x2erp.com/qdy/go-base/ctx"
7
 	"git.x2erp.com/qdy/go-base/model/response"
8
 	"git.x2erp.com/qdy/go-base/model/response"
8
 	"git.x2erp.com/qdy/go-base/webx/router"
9
 	"git.x2erp.com/qdy/go-base/webx/router"
24
 
25
 
25
 // RegisterSessionRoutes 注册会话管理路由
26
 // RegisterSessionRoutes 注册会话管理路由
26
 func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
27
 func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
27
-	// 创建会话
28
+	// 创建会话(需要Token认证)
28
 	ws.POST("/api/session/create",
29
 	ws.POST("/api/session/create",
29
 		func(req *SessionCreateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[SessionResponse], error) {
30
 		func(req *SessionCreateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[SessionResponse], error) {
30
 			session, err := client.CreateSession(ctx, req.Title)
31
 			session, err := client.CreateSession(ctx, req.Title)
45
 				},
46
 				},
46
 			}, nil
47
 			}, nil
47
 		},
48
 		},
48
-	).Desc("创建新的 opencode 会话").Register()
49
+	).Use(authbase.TokenAuth).Desc("创建新的 opencode 会话").Register()
49
 
50
 
50
-	// 获取会话列表
51
+	// 获取会话列表(需要Token认证)
51
 	ws.GET("/api/session/list",
52
 	ws.GET("/api/session/list",
52
 		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[[]opencode.Session], error) {
53
 		func(ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[[]opencode.Session], error) {
53
 			sessions, err := client.ListSessions(ctx)
54
 			sessions, err := client.ListSessions(ctx)
63
 				Data:    sessions,
64
 				Data:    sessions,
64
 			}, nil
65
 			}, nil
65
 		},
66
 		},
66
-	).Desc("获取 opencode 会话列表").Register()
67
+	).Use(authbase.TokenAuth).Desc("获取 opencode 会话列表").Register()
67
 
68
 
68
 	// 获取单个会话(暂不实现,因为 opencode API 可能不支持)
69
 	// 获取单个会话(暂不实现,因为 opencode API 可能不支持)
69
 	// ws.GET("/api/session/get",
70
 	// ws.GET("/api/session/get",

+ 18
- 2
main.go Просмотреть файл

14
 	"git.x2erp.com/qdy/go-base/graceful"
14
 	"git.x2erp.com/qdy/go-base/graceful"
15
 	"git.x2erp.com/qdy/go-base/logger"
15
 	"git.x2erp.com/qdy/go-base/logger"
16
 	"git.x2erp.com/qdy/go-base/model/response"
16
 	"git.x2erp.com/qdy/go-base/model/response"
17
+	"git.x2erp.com/qdy/go-base/sdk/configure"
17
 	"git.x2erp.com/qdy/go-base/webx"
18
 	"git.x2erp.com/qdy/go-base/webx"
18
 	"git.x2erp.com/qdy/go-base/webx/router"
19
 	"git.x2erp.com/qdy/go-base/webx/router"
19
 
20
 
72
 		log.Fatalf("创建 opencode 客户端失败: %v", err)
73
 		log.Fatalf("创建 opencode 客户端失败: %v", err)
73
 	}
74
 	}
74
 
75
 
76
+	// 4.1 创建配置中心客户端
77
+	var configClient *configure.Client
78
+	configClient, err = configure.NewClient()
79
+	if err != nil {
80
+		log.Printf("警告: 创建配置中心客户端失败,登录功能将不可用: %v", err)
81
+		configClient = nil
82
+	} else {
83
+		log.Printf("配置中心客户端创建成功,BaseURL: %s", configClient.GetConfig().BaseURL)
84
+	}
85
+
75
 	// 5. 得到 webservice 服务工厂
86
 	// 5. 得到 webservice 服务工厂
76
 	webxFactory := webx.GetWebServiceFactory()
87
 	webxFactory := webx.GetWebServiceFactory()
77
 
88
 
85
 	routerService := router.NewWebService(webService.GetRouter())
96
 	routerService := router.NewWebService(webService.GetRouter())
86
 
97
 
87
 	// 8. 注册路由--api
98
 	// 8. 注册路由--api
88
-	registerRoutes(routerService, client, webService)
99
+	registerRoutes(routerService, client, configClient, webService)
89
 
100
 
90
 	// 9. 注册前端静态文件服务
101
 	// 9. 注册前端静态文件服务
91
 	registerStaticFiles(webService)
102
 	registerStaticFiles(webService)
159
 }
170
 }
160
 
171
 
161
 // registerRoutes 注册所有 API 路由
172
 // registerRoutes 注册所有 API 路由
162
-func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, webService *webx.WebService) {
173
+func registerRoutes(ws *router.RouterService, client opencode.OpenCodeClient, configClient *configure.Client, webService *webx.WebService) {
163
 	// 会话管理路由
174
 	// 会话管理路由
164
 	routes.RegisterSessionRoutes(ws, client)
175
 	routes.RegisterSessionRoutes(ws, client)
165
 
176
 
174
 		webService.GetRouter().Handle("/api/logs/stream", routes.LogStreamHandler(opencodeProcess))
185
 		webService.GetRouter().Handle("/api/logs/stream", routes.LogStreamHandler(opencodeProcess))
175
 	}
186
 	}
176
 
187
 
188
+	// 认证路由
189
+	if configClient != nil {
190
+		routes.RegisterAuthRoutes(ws, configClient)
191
+	}
192
+
177
 	// 健康检查路由
193
 	// 健康检查路由
178
 	ws.GET("/api/health", func(reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
194
 	ws.GET("/api/health", func(reqCtx *ctx.RequestContext) (*response.QueryResult[map[string]interface{}], error) {
179
 		result := map[string]interface{}{
195
 		result := map[string]interface{}{

+ 341
- 0
test/auth_test.go Просмотреть файл

1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"os/exec"
11
+	"testing"
12
+	"time"
13
+
14
+	"git.x2erp.com/qdy/go-base/model/request/queryreq"
15
+	"git.x2erp.com/qdy/go-base/sdk/configure"
16
+)
17
+
18
+// TestAuthLogin 测试认证登录功能
19
+func TestAuthLogin(t *testing.T) {
20
+	// 清理测试缓存
21
+	cleanTestCache(t)
22
+
23
+	// 获取svc-code服务地址
24
+	svcCodeURL := "http://localhost:8020"
25
+
26
+	// 检查svc-code服务是否运行
27
+	if !isServiceRunning(t, svcCodeURL) {
28
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
29
+	}
30
+
31
+	// 检查配置中心服务是否运行
32
+	configureURL := "http://localhost:8080"
33
+	if !isServiceRunning(t, configureURL) {
34
+		t.Skipf("配置中心服务未运行在 %s,跳过测试", configureURL)
35
+	}
36
+
37
+	// 测试1:使用SDK直接登录(验证SDK功能)
38
+	t.Run("SDKLogin", func(t *testing.T) {
39
+		testSDKLogin(t, configureURL)
40
+	})
41
+
42
+	// 测试2:通过svc-code API登录(验证集成功能)
43
+	t.Run("APILogin", func(t *testing.T) {
44
+		testAPILogin(t, svcCodeURL, configureURL)
45
+	})
46
+
47
+	// 测试3:无效凭证登录测试
48
+	t.Run("InvalidCredentials", func(t *testing.T) {
49
+		testInvalidCredentials(t, svcCodeURL)
50
+	})
51
+}
52
+
53
+// testSDKLogin 测试直接使用SDK登录配置中心
54
+func testSDKLogin(t *testing.T, configureURL string) {
55
+	// 创建SDK客户端(登录无需认证,但需要BaseURL)
56
+	client, err := configure.NewBasicAuthClient(configureURL, "test", "test")
57
+	if err != nil {
58
+		t.Fatalf("创建SDK客户端失败: %v", err)
59
+	}
60
+
61
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
62
+	defer cancel()
63
+
64
+	// 使用测试用户登录
65
+	req := &configure.UserLoginRequest{
66
+		UserID:   "test-user-001",
67
+		Password: "password123",
68
+	}
69
+
70
+	startTime := time.Now()
71
+	token, err := client.LoginUser(ctx, req)
72
+	elapsed := time.Since(startTime)
73
+
74
+	if err != nil {
75
+		if isConnectionError(err) {
76
+			t.Skipf("配置中心连接失败,跳过测试: %v", err)
77
+		}
78
+		t.Fatalf("SDK用户登录失败: %v (耗时: %v)", err, elapsed)
79
+	}
80
+
81
+	if token == "" {
82
+		t.Fatal("SDK用户登录返回空token")
83
+	}
84
+
85
+	t.Logf("SDK用户登录成功!获取Token: %s... (耗时: %v)",
86
+		token[:min(50, len(token))], elapsed)
87
+
88
+	// 验证token可用于创建新的认证客户端
89
+	tokenClient, err := configure.NewTokenAuthClient(configureURL, token)
90
+	if err != nil {
91
+		t.Fatalf("使用获取的token创建客户端失败: %v", err)
92
+	}
93
+
94
+	t.Logf("Token客户端创建成功,BaseURL: %s", tokenClient.GetConfig().BaseURL)
95
+}
96
+
97
+// testAPILogin 测试通过svc-code API登录
98
+func testAPILogin(t *testing.T, svcCodeURL, configureURL string) {
99
+	// 创建HTTP客户端
100
+	httpClient := &http.Client{
101
+		Timeout: 30 * time.Second,
102
+	}
103
+
104
+	// 准备登录请求
105
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
106
+	loginData := map[string]string{
107
+		"user_id":  "test-user-001",
108
+		"password": "password123",
109
+	}
110
+	loginJSON, err := json.Marshal(loginData)
111
+	if err != nil {
112
+		t.Fatalf("JSON序列化失败: %v", err)
113
+	}
114
+
115
+	// 发送登录请求
116
+	startTime := time.Now()
117
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(loginJSON))
118
+	if err != nil {
119
+		t.Fatalf("创建HTTP请求失败: %v", err)
120
+	}
121
+	req.Header.Set("Content-Type", "application/json")
122
+
123
+	resp, err := httpClient.Do(req)
124
+	if err != nil {
125
+		t.Fatalf("发送登录请求失败: %v", err)
126
+	}
127
+	defer resp.Body.Close()
128
+
129
+	elapsed := time.Since(startTime)
130
+
131
+	// 检查响应状态码
132
+	if resp.StatusCode != 200 {
133
+		body, _ := io.ReadAll(resp.Body)
134
+		t.Fatalf("登录API返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
135
+	}
136
+
137
+	// 解析响应
138
+	body, err := io.ReadAll(resp.Body)
139
+	if err != nil {
140
+		t.Fatalf("读取响应体失败: %v", err)
141
+	}
142
+
143
+	var result map[string]interface{}
144
+	if err := json.Unmarshal(body, &result); err != nil {
145
+		t.Fatalf("解析响应JSON失败: %v, 响应体: %s", err, string(body))
146
+	}
147
+
148
+	// 检查响应结构
149
+	if !result["success"].(bool) {
150
+		t.Fatalf("登录失败: %v", result)
151
+	}
152
+
153
+	token, ok := result["data"].(string)
154
+	if !ok || token == "" {
155
+		t.Fatalf("响应中未找到有效token: %v", result)
156
+	}
157
+
158
+	t.Logf("API用户登录成功!获取Token: %s... (耗时: %v)",
159
+		token[:min(50, len(token))], elapsed)
160
+
161
+	// 验证token可用于配置中心
162
+	t.Run("ValidateTokenWithConfigure", func(t *testing.T) {
163
+		validateTokenWithConfigure(t, configureURL, token)
164
+	})
165
+}
166
+
167
+// testInvalidCredentials 测试无效凭证登录
168
+func testInvalidCredentials(t *testing.T, svcCodeURL string) {
169
+	httpClient := &http.Client{
170
+		Timeout: 10 * time.Second,
171
+	}
172
+
173
+	loginURL := fmt.Sprintf("%s/api/auth/login", svcCodeURL)
174
+	invalidData := map[string]string{
175
+		"user_id":  "invalid-user",
176
+		"password": "wrong-password",
177
+	}
178
+	invalidJSON, err := json.Marshal(invalidData)
179
+	if err != nil {
180
+		t.Fatalf("JSON序列化失败: %v", err)
181
+	}
182
+
183
+	req, err := http.NewRequest("POST", loginURL, bytes.NewReader(invalidJSON))
184
+	if err != nil {
185
+		t.Fatalf("创建HTTP请求失败: %v", err)
186
+	}
187
+	req.Header.Set("Content-Type", "application/json")
188
+
189
+	resp, err := httpClient.Do(req)
190
+	if err != nil {
191
+		t.Fatalf("发送登录请求失败: %v", err)
192
+	}
193
+	defer resp.Body.Close()
194
+
195
+	// 即使凭证无效,API也应该返回200(业务错误通过success字段表示)
196
+	if resp.StatusCode != 200 {
197
+		body, _ := io.ReadAll(resp.Body)
198
+		t.Fatalf("无效凭证登录返回非200状态码: %d, 响应: %s", resp.StatusCode, string(body))
199
+	}
200
+
201
+	body, err := io.ReadAll(resp.Body)
202
+	if err != nil {
203
+		t.Fatalf("读取响应体失败: %v", err)
204
+	}
205
+
206
+	var result map[string]interface{}
207
+	if err := json.Unmarshal(body, &result); err != nil {
208
+		t.Fatalf("解析响应JSON失败: %v", err)
209
+	}
210
+
211
+	// 无效凭证应该返回success=false
212
+	if result["success"].(bool) {
213
+		t.Fatal("无效凭证登录应该失败,但成功了")
214
+	}
215
+
216
+	t.Logf("无效凭证登录失败(预期): %v", result)
217
+}
218
+
219
+// validateTokenWithConfigure 验证token可用于配置中心
220
+func validateTokenWithConfigure(t *testing.T, configureURL, token string) {
221
+	// 使用token创建配置中心客户端
222
+	client, err := configure.NewTokenAuthClient(configureURL, token)
223
+	if err != nil {
224
+		t.Fatalf("使用token创建配置中心客户端失败: %v", err)
225
+	}
226
+
227
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
228
+	defer cancel()
229
+
230
+	// 尝试查询表列表(验证token权限)
231
+	query := &configure.DicTableQueryRequest{
232
+		QueryRequest: queryreq.QueryRequest{
233
+			Page:     0,
234
+			PageSize: 5,
235
+		},
236
+	}
237
+
238
+	result, err := client.ListTables(ctx, query)
239
+	if err != nil {
240
+		// 如果token无效或权限不足,可能失败
241
+		t.Logf("使用token查询表列表失败(可能是token权限问题): %v", err)
242
+	} else {
243
+		t.Logf("使用token查询表列表成功,总记录数: %d", result.TotalCount)
244
+	}
245
+}
246
+
247
+// cleanTestCache 清理测试缓存
248
+func cleanTestCache(t *testing.T) {
249
+	cmd := exec.Command("go", "clean", "-testcache")
250
+	if err := cmd.Run(); err != nil {
251
+		t.Logf("清除测试缓存失败: %v", err)
252
+	}
253
+}
254
+
255
+// isServiceRunning 检查服务是否运行
256
+func isServiceRunning(t *testing.T, url string) bool {
257
+	client := &http.Client{
258
+		Timeout: 3 * time.Second,
259
+	}
260
+
261
+	// 尝试访问健康检查端点或根路径
262
+	healthURL := url + "/health"
263
+	resp, err := client.Get(healthURL)
264
+	if err != nil {
265
+		// 也尝试根路径
266
+		resp, err = client.Get(url)
267
+		if err != nil {
268
+			return false
269
+		}
270
+	}
271
+	defer resp.Body.Close()
272
+
273
+	return resp.StatusCode == 200 || resp.StatusCode == 404
274
+}
275
+
276
+// isConnectionError 检查错误是否是连接错误
277
+func isConnectionError(err error) bool {
278
+	errStr := err.Error()
279
+	return contains(errStr, "connection refused") ||
280
+		contains(errStr, "connect: connection refused") ||
281
+		contains(errStr, "dial tcp") ||
282
+		contains(errStr, "EOF") ||
283
+		contains(errStr, "timeout")
284
+}
285
+
286
+// contains 检查字符串是否包含子串
287
+func contains(s, substr string) bool {
288
+	return len(s) >= len(substr) && (s == substr || (len(s) > 0 && len(substr) > 0 && (s[0:len(substr)] == substr || contains(s[1:], substr))))
289
+}
290
+
291
+// min 返回两个整数的最小值
292
+func min(a, b int) int {
293
+	if a < b {
294
+		return a
295
+	}
296
+	return b
297
+}
298
+
299
+// TestHealthEndpoint 测试健康检查端点
300
+func TestHealthEndpoint(t *testing.T) {
301
+	svcCodeURL := "http://localhost:8020"
302
+
303
+	if !isServiceRunning(t, svcCodeURL) {
304
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
305
+	}
306
+
307
+	client := &http.Client{
308
+		Timeout: 10 * time.Second,
309
+	}
310
+
311
+	resp, err := client.Get(svcCodeURL + "/api/health")
312
+	if err != nil {
313
+		t.Fatalf("访问健康检查端点失败: %v", err)
314
+	}
315
+	defer resp.Body.Close()
316
+
317
+	if resp.StatusCode != 200 {
318
+		t.Fatalf("健康检查端点返回非200状态码: %d", resp.StatusCode)
319
+	}
320
+
321
+	body, err := io.ReadAll(resp.Body)
322
+	if err != nil {
323
+		t.Fatalf("读取响应体失败: %v", err)
324
+	}
325
+
326
+	var result map[string]interface{}
327
+	if err := json.Unmarshal(body, &result); err != nil {
328
+		t.Fatalf("解析响应JSON失败: %v", err)
329
+	}
330
+
331
+	if !result["success"].(bool) {
332
+		t.Fatalf("健康检查返回success=false: %v", result)
333
+	}
334
+
335
+	data := result["data"].(map[string]interface{})
336
+	if data["status"] != "healthy" {
337
+		t.Fatalf("健康检查状态不是healthy: %v", data)
338
+	}
339
+
340
+	t.Logf("健康检查端点测试通过: %v", data)
341
+}

Загрузка…
Отмена
Сохранить