feat: 网关按域路由——验签拦门、剥伪造内部头、Authorization 原样转发
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user