feat: align svc.plus gateway layering (#630)
This commit is contained in:
parent
6575449e0d
commit
bb84b65845
46
dashboard/app/api/agent/[...segments]/route.ts
Normal file
46
dashboard/app/api/agent/[...segments]/route.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||
import { getInternalServerServiceBaseUrl } from '@lib/serviceConfig'
|
||||
|
||||
const AGENT_PREFIX = '/api/agent'
|
||||
|
||||
function createHandler() {
|
||||
const upstreamBaseUrl = getInternalServerServiceBaseUrl()
|
||||
return createUpstreamProxyHandler({
|
||||
upstreamBaseUrl,
|
||||
upstreamPathPrefix: AGENT_PREFIX,
|
||||
})
|
||||
}
|
||||
|
||||
const handler = createHandler()
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function POST(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function PUT(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function PATCH(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function DELETE(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function HEAD(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function OPTIONS(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
46
dashboard/app/api/task/[...segments]/route.ts
Normal file
46
dashboard/app/api/task/[...segments]/route.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import { createUpstreamProxyHandler } from '@lib/apiProxy'
|
||||
import { getInternalServerServiceBaseUrl } from '@lib/serviceConfig'
|
||||
|
||||
const TASK_PREFIX = '/api/task'
|
||||
|
||||
function createHandler() {
|
||||
const upstreamBaseUrl = getInternalServerServiceBaseUrl()
|
||||
return createUpstreamProxyHandler({
|
||||
upstreamBaseUrl,
|
||||
upstreamPathPrefix: TASK_PREFIX,
|
||||
})
|
||||
}
|
||||
|
||||
const handler = createHandler()
|
||||
|
||||
export function GET(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function POST(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function PUT(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function PATCH(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function DELETE(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function HEAD(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
|
||||
export function OPTIONS(request: NextRequest) {
|
||||
return handler(request)
|
||||
}
|
||||
107
dashboard/lib/apiProxy.ts
Normal file
107
dashboard/lib/apiProxy.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const DEFAULT_FORWARD_HEADERS = [
|
||||
'accept',
|
||||
'accept-language',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'cookie',
|
||||
'user-agent',
|
||||
'x-account-session',
|
||||
'x-forwarded-for',
|
||||
'x-request-id',
|
||||
'x-trace-id',
|
||||
] as const
|
||||
|
||||
const BODYLESS_METHODS = new Set(['GET', 'HEAD'])
|
||||
|
||||
type ProxyOptions = {
|
||||
upstreamBaseUrl: string
|
||||
upstreamPathPrefix: string
|
||||
allowedHeaders?: readonly string[]
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value: string): string {
|
||||
return value.endsWith('/') ? value.slice(0, -1) : value
|
||||
}
|
||||
|
||||
function buildTargetUrl(request: NextRequest, { upstreamBaseUrl, upstreamPathPrefix }: ProxyOptions): string {
|
||||
const normalizedBase = stripTrailingSlash(upstreamBaseUrl)
|
||||
const normalizedPrefix = upstreamPathPrefix.startsWith('/') ? upstreamPathPrefix : `/${upstreamPathPrefix}`
|
||||
const suffix = request.nextUrl.pathname.slice(normalizedPrefix.length)
|
||||
const normalizedSuffix = suffix.startsWith('/') ? suffix : suffix ? `/${suffix}` : ''
|
||||
const search = request.nextUrl.search ?? ''
|
||||
return `${normalizedBase}${normalizedPrefix}${normalizedSuffix}${search}`
|
||||
}
|
||||
|
||||
function buildForwardHeaders(request: NextRequest, allowedHeaders: readonly string[] = DEFAULT_FORWARD_HEADERS) {
|
||||
const headers = new Headers()
|
||||
for (const name of allowedHeaders) {
|
||||
const value = request.headers.get(name)
|
||||
if (value) {
|
||||
headers.set(name, value)
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function applySetCookieHeaders(source: Headers, target: Headers) {
|
||||
const getSetCookie = (source as Headers & { getSetCookie?: () => string[] }).getSetCookie
|
||||
if (typeof getSetCookie === 'function') {
|
||||
for (const cookie of getSetCookie.call(source)) {
|
||||
target.append('set-cookie', cookie)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const cookie = source.get('set-cookie')
|
||||
if (cookie) {
|
||||
target.append('set-cookie', cookie)
|
||||
}
|
||||
}
|
||||
|
||||
export async function proxyRequestToUpstream(request: NextRequest, options: ProxyOptions): Promise<Response> {
|
||||
const targetUrl = buildTargetUrl(request, options)
|
||||
const forwardHeaders = buildForwardHeaders(request, options.allowedHeaders)
|
||||
|
||||
let body: ArrayBuffer | undefined
|
||||
if (!BODYLESS_METHODS.has(request.method.toUpperCase())) {
|
||||
body = await request.arrayBuffer()
|
||||
}
|
||||
|
||||
let upstreamResponse: Response
|
||||
try {
|
||||
upstreamResponse = await fetch(targetUrl, {
|
||||
method: request.method,
|
||||
headers: forwardHeaders,
|
||||
body: body ? Buffer.from(body) : undefined,
|
||||
cache: 'no-store',
|
||||
redirect: 'manual',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Proxy request failed', error)
|
||||
return NextResponse.json({ error: 'upstream_unreachable' }, { status: 502 })
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers()
|
||||
upstreamResponse.headers.forEach((value, key) => {
|
||||
if (key.toLowerCase() === 'set-cookie') {
|
||||
return
|
||||
}
|
||||
responseHeaders.set(key, value)
|
||||
})
|
||||
applySetCookieHeaders(upstreamResponse.headers, responseHeaders)
|
||||
responseHeaders.set('Cache-Control', 'no-store')
|
||||
|
||||
return new Response(upstreamResponse.body, {
|
||||
status: upstreamResponse.status,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
export function createUpstreamProxyHandler(options: ProxyOptions) {
|
||||
return function handler(request: NextRequest) {
|
||||
return proxyRequestToUpstream(request, options)
|
||||
}
|
||||
}
|
||||
@ -51,7 +51,24 @@ const nextConfig = {
|
||||
trailingSlash: false,
|
||||
reactStrictMode: true,
|
||||
compress: false, // 压缩交给 Nginx,省 Node CPU
|
||||
poweredByHeader: false,
|
||||
images: { unoptimized: true }, // 关闭服务端图片处理
|
||||
httpAgentOptions: {
|
||||
keepAlive: true,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'no-store',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
96
docs/deployments/network-gateway-plan.md
Normal file
96
docs/deployments/network-gateway-plan.md
Normal file
@ -0,0 +1,96 @@
|
||||
# svc.plus 三层网关与域名规划
|
||||
|
||||
本文档描述 `svc.plus` 在生产与测试环境下的前端、网关与后端服务拆分方案,确保请求路径、域名与安全策略保持一致。架构遵循「Internet → Nginx/OpenResty → Next.js BFF → Go 后端」的分层原则。
|
||||
|
||||
## 1. 总体拓扑
|
||||
|
||||
```
|
||||
Internet
|
||||
│ 443/TLS
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Nginx / OpenResty │
|
||||
│ • TLS / HTTP2 / HSTS │
|
||||
│ • Gzip / 缓存 / 访问控制 │
|
||||
│ • 按域名与路径分流 │
|
||||
└──────────────────────────────┘
|
||||
│ 3001 (intranet)
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Next.js Dashboard (BFF) │
|
||||
│ • SSR / UI / i18n │
|
||||
│ • /api/auth/*、/api/agent/* │
|
||||
│ 统一转发与会话校验 │
|
||||
└──────────────────────────────┘
|
||||
├──────────────────┬──────────────────┤
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌────────────────┐
|
||||
│ accounts.svc │ │ api.svc │
|
||||
│ Go @ :8080 │ │ Go @ :8090 │
|
||||
│ Auth / MFA 等 │ │ Agent / Task │
|
||||
└───────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
## 2. 域名与端口映射
|
||||
|
||||
| 域名 | 环境 | 内部转发 | 主要路径 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `www.svc.plus` | 生产 | `http://127.0.0.1:3001` | `/`, `/api/*` | Dashboard 入口;所有认证与 Agent API 先进入 Next.js。 |
|
||||
| `dev.svc.plus` | 测试(SIT) | `http://127.0.0.1:3001` | `/`, `/api/*` | 测试版 Dashboard,逻辑与生产一致。 |
|
||||
| `accounts.svc.plus` | 生产 | `http://127.0.0.1:8080` | `/api/auth/*` | Go 账号服务,供 BFF 或第三方系统调用。 |
|
||||
| `accounts-dev.svc.plus` | 测试 | `http://127.0.0.1:8080` | `/api/auth/*` | 测试账号服务,TLS 证书沿用 `*.svc.plus`。 |
|
||||
| `api.svc.plus` | 生产 | `http://127.0.0.1:8090` | `/api/agent/*`, `/api/task/*` | Go 业务服务;BFF 也会访问该地址。 |
|
||||
| `dev-api.svc.plus` | 测试 | `http://127.0.0.1:8090` | `/api/agent/*`, `/api/task/*` | 测试版业务服务。 |
|
||||
| `dl.svc.plus` | 全部 | 静态目录 | `/packages/*` | 离线包及大体积静态文件,直接由 Nginx 提供。 |
|
||||
| `docs.svc.plus` | 全部 | 静态目录 | `/*` | 文档与门户,完全静态化部署。 |
|
||||
|
||||
> **命名规范**:测试域名前缀统一使用 `dev-`(例如 `dev-api.svc.plus`、`accounts-dev.svc.plus`),便于证书与路由管理。
|
||||
|
||||
## 3. 路由策略
|
||||
|
||||
| 分类 | 路由策略 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| Auth 类 (`/api/auth/*`) | 必经 Next.js,由 BFF 校验并注入 `xc_session` 等 Cookie,再转发至账号服务。 | 防止浏览器直接暴露账号服务,集中会话治理。 |
|
||||
| Agent / Task (`/api/agent/*`, `/api/task/*`) | 浏览器流量先到 Next.js,由 BFF 附加用户上下文后转发;机器到机器调用可以直接命中 `api.svc.plus`。 | 平衡性能与权限控制。 |
|
||||
| 静态资源 (`/_next/static/*`, `/public/*`) | 由 Nginx/OpenResty 缓存与压缩,Next.js 仅负责构建。 | 减少 Node 负载。 |
|
||||
| 离线包/下载 (`/packages/*`) | 独立域名 `dl.svc.plus`,支持大文件断点续传与 CDN。 | 避免 Next.js 进程被大文件占用。 |
|
||||
| 文档 (`docs.svc.plus`) | 完全静态化,不经过 Node。 | 降低安全面。 |
|
||||
|
||||
## 4. 配置要点
|
||||
|
||||
### 4.1 Nginx/OpenResty
|
||||
|
||||
- 在 `http` 块内声明复用连接的 `upstream`:`next_dashboard`(3001)、`account_service`(8080)、`api_service`(8090)。
|
||||
- `www.svc.plus` / `dev.svc.plus` 站点:
|
||||
- `/_next/static/`、`/public/` 直接回源 Next.js 并开启长缓存。
|
||||
- `/api/auth/*`、`/api/agent/*`、`/api/task/*` 统一代理至 Next.js,由 BFF 处理。
|
||||
- 其余 `/api/*` 若为透传接口,可按需再细分到 `api_service`。
|
||||
- 子域名 `accounts*.svc.plus`、`api*.svc.plus` 独立 `server`,启用 HSTS、CORS 及必要的超时时间。
|
||||
|
||||
### 4.2 Next.js Dashboard
|
||||
|
||||
- 默认监听 `3001`,保持 `compress: false`,由 Nginx 提供压缩。
|
||||
- 在 `next.config.js` 中:
|
||||
- 启用 `httpAgentOptions.keepAlive`,减少对 Go 服务的建立连接开销。
|
||||
- 通过 `headers()` 对 `/api/*` 设置 `Cache-Control: no-store`,避免代理层缓存敏感数据。
|
||||
- BFF 侧新增 `/api/agent/*` 与 `/api/task/*` Catch-all Route,复用会话信息并将请求转发给 `api_service`。
|
||||
|
||||
### 4.3 Go 后端
|
||||
|
||||
- `accounts` 服务负责认证、MFA、密码重置,不直接暴露在 Dashboard 主域名下。
|
||||
- `api` 服务聚合 Agent、任务、日志等业务接口,可接受来自 BFF 或专用域名的调用。
|
||||
- 两个服务都暴露 `/metrics`,供 `otel.svc.plus` 或 Prometheus 抓取。
|
||||
|
||||
## 5. 监控与证书
|
||||
|
||||
- TLS 统一使用 `*.svc.plus` 泛域名证书,可覆盖测试与生产子域。
|
||||
- Prometheus 抓取地址建议:
|
||||
- `https://accounts.svc.plus/metrics`
|
||||
- `https://api.svc.plus/metrics`
|
||||
- BFF 层应在日志中记录 `X-Request-ID` / `X-B3-TraceId` 等链路标识,便于串联 Nginx 与后端日志。
|
||||
|
||||
## 6. 变更清单
|
||||
|
||||
- 测试环境沿用 `dev-` 前缀的域名,配置与生产一致,仅监听端口与证书路径不同。
|
||||
- Nginx 示例文件(`example/prod/nginx`、`example/sit/nginx`、`example/macos/openresty`)已按上述策略更新,部署时可直接替换原有示例并根据实际路径调整证书位置。
|
||||
|
||||
@ -16,6 +16,21 @@ http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
upstream next_dashboard {
|
||||
server 127.0.0.1:3001;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
upstream account_service {
|
||||
server 127.0.0.1:8080;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
upstream api_service {
|
||||
server 127.0.0.1:8090;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
|
||||
@ -24,13 +24,14 @@ server {
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_pass http://account_service;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
|
||||
|
||||
@ -24,13 +24,14 @@ server {
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_pass http://api_service;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
|
||||
|
||||
@ -9,6 +9,21 @@ http {
|
||||
default_type application/octet-stream;
|
||||
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
|
||||
|
||||
upstream next_dashboard {
|
||||
server 127.0.0.1:3001;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream account_service {
|
||||
server 127.0.0.1:8080;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream api_service {
|
||||
server 127.0.0.1:8090;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
|
||||
@ -14,13 +14,14 @@ server {
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_pass http://account_service;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
|
||||
|
||||
@ -18,13 +18,14 @@ server {
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_pass http://api_service;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
|
||||
|
||||
@ -17,7 +17,7 @@ server {
|
||||
# Next.js 静态资源 (_next/static/*)
|
||||
# ================================
|
||||
location /_next/static/ {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_pass http://next_dashboard;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
@ -45,7 +45,7 @@ server {
|
||||
# API / SSR 页面
|
||||
# ================================
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_pass http://next_dashboard;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@ -9,6 +9,21 @@ http {
|
||||
default_type application/octet-stream;
|
||||
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
|
||||
|
||||
upstream next_dashboard {
|
||||
server 127.0.0.1:3001;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream account_service {
|
||||
server 127.0.0.1:8080;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream api_service {
|
||||
server 127.0.0.1:8090;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user