/** * 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; 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; } }