feat: align svc.plus gateway layering (#630)

This commit is contained in:
shenlan 2025-11-02 07:11:11 +08:00 committed by GitHub
parent 6575449e0d
commit bb84b65845
13 changed files with 367 additions and 6 deletions

View 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)
}

View 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
View 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)
}
}

View File

@ -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 [
{

View 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`)已按上述策略更新,部署时可直接替换原有示例并根据实际路径调整证书位置。

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;