diff --git a/admin-server/gateway.go b/admin-server/gateway.go index faa2b45..99a638a 100644 --- a/admin-server/gateway.go +++ b/admin-server/gateway.go @@ -257,6 +257,8 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error { writeErrorPages(b, routeKind(r) == "portal") if routeKind(r) == "portal" { + // 静态门户页也要给出确定性探测结论(免认证,避免 302 到登录页) + b.WriteString(" location = /.navi-ping { auth_request off; access_log off; return 204; }\n") b.WriteString(" index index.html;\n location / { try_files $uri $uri/ /404.html; }\n}\n\n") return nil } @@ -265,6 +267,19 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error { r.Upstream = "http://127.0.0.1:3005" r.RequireLogin = false } + // 可达性探测路径:Dashy 用 no-cors 探测条目 URL,若命中统一登录的 302 跳转 + // (跳到另一个域)浏览器会直接报错,检查就永远停在"进行中"(表现为全黄)。 + // 这里给每条路由生成一个免认证的固定路径,并且**反代到真实上游**: + // 上游正常则 2xx(绿),上游挂了则 5xx(红),语义正确。 + b.WriteString(" location = /.navi-ping {\n auth_request off;\n access_log off;\n") + if routeKind(r) == "portal" { + b.WriteString(" return 204;\n }\n") + } else { + fmt.Fprintf(b, " proxy_pass %s/;\n", strings.TrimRight(strings.TrimSpace(r.Upstream), "/")) + b.WriteString(" proxy_set_header Host $host;\n") + b.WriteString(" proxy_connect_timeout 4s;\n proxy_read_timeout 4s;\n") + b.WriteString(" proxy_redirect off;\n }\n") + } if r.RequireLogin { if strings.TrimSpace(cfg.AuthURL) == "" || strings.TrimSpace(cfg.LoginURL) == "" { return fmt.Errorf("%s: 需要认证但未配置认证/登录地址", host) diff --git a/admin-server/gateway_nav.go b/admin-server/gateway_nav.go index 9a7cbbb..3805290 100644 --- a/admin-server/gateway_nav.go +++ b/admin-server/gateway_nav.go @@ -104,7 +104,9 @@ func syncGatewayNavigation(gw *GatewayConfig) error { if icon == "" { icon = "fas fa-globe" } + statusOn := true item := NavItem{ + StatusCheck: &statusOn, Title: strings.TrimSpace(r.NavName), Description: strings.TrimSpace(r.NavDescription), Icon: icon, @@ -113,6 +115,9 @@ func syncGatewayNavigation(gw *GatewayConfig) error { gatewayManagedKey: true, gatewayHostKey: strings.ToLower(strings.TrimSpace(r.Host)), gatewayAutoIconKey: r.NavAutoIcon, + // 可达性检查指向网关生成的免认证路径:避免命中登录 302 导致 + // no-cors 探测报错、状态永远"进行中"(用户反馈的"全黄")。 + "statusCheckUrl": "https://" + strings.TrimSpace(r.Host) + "/.navi-ping", }, } dashy.Sections[idx].Items = append(dashy.Sections[idx].Items, item) diff --git a/admin-server/gateway_test.go b/admin-server/gateway_test.go index bda8d70..60de65d 100644 --- a/admin-server/gateway_test.go +++ b/admin-server/gateway_test.go @@ -358,3 +358,29 @@ func TestRenderGatewayConfErrorPagesSkipAuth(t *testing.T) { } } } + +// 每条路由都要有免认证探测路径,且必须反代到真实上游(否则无法反映服务健康)。 +func TestRenderGatewayConfPingPath(t *testing.T) { + out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 8081, AuthURL: "http://x/_auth", + LoginURL: "https://l/login", + Routes: []Route{ + {Host: "a.com", Upstream: "http://10.0.0.1:3000", RequireLogin: true}, + {Host: "p.com", Kind: "portal"}, + }}) + if err != nil { + t.Fatalf("渲染失败: %v", err) + } + if !contains(out, "location = /.navi-ping {") { + t.Fatalf("缺少探测路径:\n%s", out) + } + if !contains(out, "auth_request off;") { + t.Fatalf("探测路径必须免认证") + } + // 普通路由的探测要打在真实上游上(挂了要能红) + if !contains(out, "proxy_pass http://10.0.0.1:3000/;") { + t.Fatalf("探测未反代到上游:\n%s", out) + } + if !contains(out, "return 204;") { + t.Fatalf("portal 类型应直接 204") + } +}