feat: 网关按域路由——验签拦门、剥伪造内部头、Authorization 原样转发

This commit is contained in:
lovefyf
2026-08-13 05:04:10 +08:00
parent 6beb663cf0
commit af1c1a8eb4
4 changed files with 151 additions and 1 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { Context } from 'hono';
const STRIP_HEADERS = ['x-user-id', 'x-user-roles', 'x-internal-token', 'host', 'content-length'];
/**
* 回传时必须丢掉的下游响应头:undici 已按 content-encoding 解过压,
* body 是明文,再原样回传 content-encoding/content-length/transfer-encoding
* 就是自相矛盾的响应(客户端会二次解压失败或按错长度截断)。
*/
const STRIP_RESPONSE_HEADERS = ['content-encoding', 'content-length', 'transfer-encoding'];
/** 验签后转发:Authorization 原样带上(下游自己再验一遍——纵深防御,指南 5.6) */
export async function forward(c: Context, target: string, stripPrefix: string): Promise<Response> {
const search = new URL(c.req.url).search;
const path = c.req.path.slice(stripPrefix.length) || '/';
const headers = new Headers(c.req.raw.headers);
for (const h of STRIP_HEADERS) headers.delete(h);
const method = c.req.method;
const init: RequestInit & { duplex?: 'half' } = { method, headers };
if (method !== 'GET' && method !== 'HEAD') {
init.body = c.req.raw.body;
init.duplex = 'half'; // Node fetch 流式请求体必需
}
let res: Response;
try {
res = await fetch(new URL(path + search, target), init);
} catch {
// 下游没起/连不上:给统一错误形状的 502,并点名是哪个下游
return c.json(
{ error: 'bad_gateway', message: `下游服务 ${target} 不可达`, hint: '确认目标服务已启动' },
502,
);
}
const outHeaders = new Headers(res.headers);
for (const h of STRIP_RESPONSE_HEADERS) outHeaders.delete(h);
return new Response(res.body, { status: res.status, headers: outHeaders });
}
+22
View File
@@ -0,0 +1,22 @@
import { Hono } from 'hono';
import { config } from '../../shared/config';
import { requireAuth } from '../../shared/keycloak-auth';
import { forward } from './forward';
/** 目标地址取自 config——env+缺省只在 config.ts 读一次,此处不再二次读 env */
function target(name: 'TASK' | 'DATA' | 'AGENT'): string {
return {
TASK: config.taskServiceUrl,
DATA: config.dataServiceUrl,
AGENT: config.agentServiceUrl,
}[name];
}
export function proxyRoutes(): Hono {
const r = new Hono();
r.use('*', requireAuth());
r.all('/task/*', (c) => forward(c, target('TASK'), '/api/task'));
r.all('/data/*', (c) => forward(c, target('DATA'), '/api/data'));
r.all('/agent/*', (c) => forward(c, target('AGENT'), '/api/agent'));
return r;
}