- RouterAuthGuard: validate Router-Authorization header (ADR-036) - DataLoader factory: request-scoped batching (ADR-035) - ScopeTokenService: Redis-backed scope token (ADR-041) - GraphqlContext: build context from HTTP headers - FederationExceptionFilter: HTTP-to-GraphQL error mapping
77 lines
2.0 KiB
TypeScript
77 lines
2.0 KiB
TypeScript
/**
|
||
* RouterAuthGuard - Apollo Router 信任凭证校验
|
||
*
|
||
* 强制约束(v2.1 §3.7 / ADR-036):
|
||
* Apollo Router 请求子图时必须携带 Router-Authorization Header,
|
||
* 各服务 NestJS Guard 拦截并校验,拒绝任何非 Router 发起的 GraphQL 请求。
|
||
*
|
||
* 部署模式:
|
||
* - 生产:ROUTER_AUTH_SECRET 环境变量配置共享密钥
|
||
* - 开发:DEV_MODE=true 时跳过校验(仅限本地)
|
||
*/
|
||
import {
|
||
CanActivate,
|
||
ExecutionContext,
|
||
Injectable,
|
||
ForbiddenException,
|
||
Logger,
|
||
} from "@nestjs/common";
|
||
|
||
export const ROUTER_AUTH_HEADER = "router-authorization";
|
||
|
||
export interface RouterAuthConfig {
|
||
/** 共享密钥(生产环境由 Secret 注入) */
|
||
secret: string;
|
||
/** 开发模式跳过校验 */
|
||
devMode?: boolean;
|
||
}
|
||
|
||
@Injectable()
|
||
export class RouterAuthGuard implements CanActivate {
|
||
private readonly logger = new Logger(RouterAuthGuard.name);
|
||
private readonly config: RouterAuthConfig;
|
||
|
||
constructor(config: RouterAuthConfig) {
|
||
this.config = config;
|
||
}
|
||
|
||
canActivate(ctx: ExecutionContext): boolean {
|
||
if (this.config.devMode === true) {
|
||
return true;
|
||
}
|
||
|
||
const req = ctx.switchToHttp().getRequest<{
|
||
headers: Record<string, string | undefined>;
|
||
url: string;
|
||
}>();
|
||
|
||
// 健康检查端点豁免
|
||
if (req.url?.startsWith("/health") || req.url?.startsWith("/ready")) {
|
||
return true;
|
||
}
|
||
|
||
const routerAuth = req.headers[ROUTER_AUTH_HEADER];
|
||
const expected = this.config.secret;
|
||
|
||
if (!expected) {
|
||
this.logger.error(
|
||
`${ROUTER_AUTH_HEADER} secret not configured (ROUTER_AUTH_SECRET env required)`,
|
||
);
|
||
throw new ForbiddenException(
|
||
"Router authorization not configured on server",
|
||
);
|
||
}
|
||
|
||
if (!routerAuth || routerAuth !== expected) {
|
||
this.logger.warn(
|
||
`Direct GraphQL access denied (path=${req.url}); must go through Apollo Router`,
|
||
);
|
||
throw new ForbiddenException(
|
||
"Direct GraphQL access denied; must go through Apollo Router",
|
||
);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|