• 首页
  • little笔记小助手
  • 聊天
  • 工具
  • 登录/注册

nest如何使用代理服务

用户头像
新鲜噩梦
little笔记全栈作者
创建于:2025-08-20 17:56:56字数:12355
按理说代理服务肯定是使用nginx去做,nest做这个事总有点“狗拿耗子”的感觉。
但意外总是有的,比如需要在中转过程中需要采集一些数据埋点。

nest中如何使用代理

一般使用nest时,在 main.ts 中都会有以下配置
js
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule,
    new ExpressAdapter(),
  );
  // ...
}
当需要添加代理服务的时候,app.module.ts 添加以下代码
js
consumer
      .apply(ProxyMiddleware)
      .forRoutes({ path: 'api/proxy/*path', method: RequestMethod.ALL });
意思是 /api/proxy/ 下的所有路由都会走 ProxyMiddleware 中间件,内容为:
js
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { createProxyMiddleware, Options } from 'http-proxy-middleware';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';
import type { RequestHandler } from 'express';

import { LoggerService } from 'src/global/logger/service';

@Injectable()
export class ProxyMiddleware implements NestMiddleware {
  private proxyMiddleware!: RequestHandler;

  constructor(
    private configService: ConfigService,
    private logger: LoggerService,
  ) {
    this.initializeProxy();
  }

  private initializeProxy() {
    // 从配置文件获取代理配置
    const proxyConfig = this.configService.get('proxy') as ProxyConfig;
    console.log('代理配置:', proxyConfig);
    const { target, timeout, pathRewrite } = proxyConfig;

    const proxyOptions: Options = {
      target,
      changeOrigin: true,
      timeout, // 增加超时时间到60秒
      proxyTimeout: 60000,
      //   secure: true, // 验证SSL证书
      followRedirects: true,
      pathRewrite,
      // 使用 on 对象配置事件监听器
      on: {
        // 请求处理
        proxyReq: (proxyReq, req, res) => {
          this.logger.log('onProxyReq 回调被执行!');

          // 添加转发头信息
          const clientIP =
            req.socket?.remoteAddress ||
            (req.headers['x-forwarded-for'] as string) ||
            'unknown';
          proxyReq.setHeader('X-Forwarded-For', clientIP);
          proxyReq.setHeader(
            'X-Forwarded-Proto',
            req.headers['x-forwarded-proto'] || 'http',
          );
          proxyReq.setHeader('X-Forwarded-Host', req.headers.host || 'unknown');
          proxyReq.setHeader('X-Real-IP', clientIP);

          // 移除敏感头信息
          proxyReq.removeHeader('cookie');
          proxyReq.removeHeader('authorization');

          // 记录请求日志
          this.logger.log(`代理请求 ${req.method} ${req.url} 到 ${target}`);
        },
        // 响应处理
        proxyRes: (proxyRes, req, res) => {
          // 添加自定义响应头
          proxyRes.headers['X-Proxy-By'] = 'NestJS-MiddleProxy';
          proxyRes.headers['X-Proxy-Timestamp'] = new Date().toISOString();

          // 移除敏感响应头
          delete proxyRes.headers['set-cookie'];
          delete proxyRes.headers['server'];

          // 记录响应日志
          this.logger.log(
            `代理响应: ${proxyRes.statusCode} 请求 ${req.method} ${req.url}`,
          );
        },
        // 错误处理
        error: (err, req, res) => {
          this.logger.error(
            `代理错误 ${req.method} ${req.url}: ${err.message}`,
            err.stack,
          );
          console.error('代理错误详情:', err);

          // 检查是否为 ServerResponse 类型
          if ('headersSent' in res && !res.headersSent) {
            res.statusCode = 502;
            res.setHeader('Content-Type', 'application/json');
            res.end(
              JSON.stringify({
                error: '网关错误',
                message: `代理服务器错误: ${err.message}`,
                timestamp: new Date().toISOString(),
                path: req.url,
              }),
            );
          }
        },
        // WebSocket 代理错误处理
        proxyReqWs: (proxyReq, req, socket) => {
          socket.on('error', (err) => {
            this.logger.error(`WebSocket 代理错误: ${err.message}`);
          });
        },
      },
      // 健康检查
      router: (req) => {
        // 可以根据请求动态选择目标服务器
        return target;
      },
    };

    this.proxyMiddleware = createProxyMiddleware(proxyOptions);
  }

  use(req: Request, res: Response, next: NextFunction) {
    this.logger.log(`ProxyMiddleware被调用: ${req.method} ${req.url}`);

    try {
      // 请求验证
      if (!this.isValidRequest(req)) {
        return res.status(400).json({
          error: '请求错误',
          message: '无效的代理请求',
          timestamp: new Date().toISOString(),
        });
      }

      // 速率限制检查(可以集成 Redis 实现分布式限流)
      if (!this.checkRateLimit(req)) {
        return res.status(429).json({
          error: '请求过多',
          message: '超过速率限制',
          timestamp: new Date().toISOString(),
        });
      }

      // 执行代理
      this.proxyMiddleware(req, res, next);
      this.logger.log('代理中间件执行完成');
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : '未知错误';
      const errorStack = error instanceof Error ? error.stack : undefined;
      this.logger.error(`中间件错误: ${errorMessage}`, errorStack);
      if (!res.headersSent) {
        res.status(500).json({
          error: '内部服务器错误',
          message: '代理中间件错误',
          timestamp: new Date().toISOString(),
        });
      }
    }
  }

  private isValidRequest(req: Request): boolean {
    // 基本请求验证
    if (!req.url || req.url.length > 2048) {
      return false;
    }

    // 检查是否包含恶意路径
    const maliciousPatterns = ['../', '.\\', '<script', 'javascript:', 'data:'];
    const url = req.url.toLowerCase();

    return !maliciousPatterns.some((pattern) => url.includes(pattern));
  }

  private checkRateLimit(req: Request): boolean {
    // 简单的内存限流实现(生产环境建议使用 Redis)
    // 这里可以集成更复杂的限流逻辑
    return true;
  }
}
bash
# 代理配置
proxy:
  target: 'http://localhost:8020'
  timeout: 30000
  retries: 3
  pathRewrite:
    '^/api/proxy': ''
  rateLimit:
    windowMs: 60000 # 1分钟
    maxRequests: 100 # 每分钟最多100个请求
以上代码就是一些代理的具体逻辑,当我去使用这个代理的时候,就一直报超时错误,原因我找了好久,原来是因为body-parser的问题,他是干嘛用的?
body-parser是 Node.js/Express 生态中的中间件,可以在npm上下载使用。核心作用是解析 HTTP 请求体(Request Body)中的数据,并将解析后的数据挂载到 req.body 对象上,供后续路由 / 中间件直接使用。
简单来说,客户端(如浏览器、Postman)通过 POST/PUT 等请求发送数据(如 JSON、表单数据)时,这些数据会放在请求体里;而原生 Express 无法直接读取请求体,body-parser 就是帮你 “拆包” 并把数据整理成便于操作的格式。
Express 4.16.0 版本以前,需要单独下载body-parser这个包来做body解析。Express 4.16.0 版本以后,内置支持body-parser。
js
// 替代 body-parser.json()
app.use(express.json());
// 替代 body-parser.urlencoded()
app.use(express.urlencoded({ extended: true }));
以下为nest中默认使用body-parser的解释
当使用express适配器时,NestJS应用程序将默认注册body-parser包中的json和urlencoded中间件。这意味着,如果您想通过MiddlewareConsumer自定义该中间件,您需要在使用NestFactory.create()创建应用程序时将全局中间件关闭,方法是将bodyParser标志设置为false。
https://nestjs.inode.club/middleware
问题就在这,http-proxy-middleware这个包在转发时,需要转发原数据结构,所以经过body-parser处理的body数据不能直接用于转发!
所以必须要关掉全局的body-parser,有了如下配置:
js
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule,
    new ExpressAdapter(),
    { bodyParser: false },
  );
代理可以正常发送了。
新的问题产生了
全局的去掉了,本服务之前定义的那些的post接口获取不到body了,怎么办?
需要自定义配置body-parser,方法如下:
修改 app.module.ts,添加多路由配置:
js
import {
  JsonBodyParserMiddleware,
  UrlEncodedBodyParserMiddleware,
} from 'src/middleware/body-parser';

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(ProxyMiddleware)
      .forRoutes({ path: 'api/proxy/*path', method: RequestMethod.ALL });

    consumer
      .apply(
        JsonBodyParserMiddleware,
        UrlEncodedBodyParserMiddleware,
      )
      .exclude({
        path: 'api/proxy/*path',
        method: RequestMethod.ALL,
      })
      .forRoutes('*');
  }
}
增加的JsonBodyParserMiddleware 和UrlEncodedBodyParserMiddleware 方法如下:
src/middleware/body-parser.ts
js
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as express from 'express';

@Injectable()
export class JsonBodyParserMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    express.json()(req, res, next);
  }
}

@Injectable()
export class UrlEncodedBodyParserMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    express.urlencoded({ extended: true })(req, res, next);
  }
}
新的问题又出现
如果我想在代理中把请求的参数和相应的参数打印出来,该怎么做?
如果是get请求,很简单,直接打印url就好了,因为get的参数都在url上。
如果是post请求,就完了,因为代理中已经去掉了 body-parser ,所以想要获取body中的数据需要自己来实现:
js
proxyReq: (proxyReq, req, res) => {
  let body = '';
  
  // 监听数据流
  req.on('data', (chunk) => {
    body += chunk;
  });
  
  // 数据接收完成
  req.on('end', () => {
    console.log('响应体内容:', body);
    // 在这里可以对响应体进行处理
  });
}
自己手撸的问题很多,最常见的问题就是乱码问题,具体的可以问问AI,总之需要一个现成的工具库来完成这个操作,这个库去哪里找?去 body-parser 的源码看看它的实现。
https://github.com/expressjs/body-parser/tree/master
93cec24a-1276-4d7a-98ec-a1417ac92f72
可以去read文件找下对应的实现:
0e190b53-1897-4008-a1bb-9ccae1bdc9ef
这里的 getBody 方法就是获取body的核心方法
1a603f65-6375-46c6-80aa-d43bce624c3e
raw-body 就是解决body的第三方包,自己可以组装一个简单的解析body的工具函数
handleHttp.ts
js
import * as rawBody from 'raw-body';
import contentType from 'content-type'; // 辅助解析 Content-Type 的工具包(需安装)
import { IncomingMessage } from 'http';

// 解析请求的 Content-Type 头
export function getCharset(req: IncomingMessage) {
  try {
    // 用 content-type 包解析头信息
    const { parameters } = contentType.parse(req);
    // 返回 charset,默认 utf-8
    return parameters.charset || 'utf-8';
  } catch (err) {
    // 解析失败时默认用 utf-8
    return 'utf-8';
  }
}

// 使用示例
export async function handleRequest(req: IncomingMessage) {
  // 获取编码
  const charset = getCharset(req);

  // 用获取到的编码解析请求体
  const str = await rawBody(req, { encoding: charset });

  return str;
}

export async function formatBody(req: IncomingMessage) {
  // 处理POST请求体 - 直接从原始请求流中读取
  let bodyStr = 'null';

  try {
    bodyStr = await handleRequest(req);
    bodyStr = JSON.stringify(JSON.parse(bodyStr));
  } catch (error) {
    console.log('formatBody-error', error);
  }

  return {
    bodyStr,
  };
}
在代理中间件中直接使用 formatBody
js
{
  // 请求处理
  proxyReq: async (proxyReq, req, res) => {
    this.logger.log('onProxyReq 回调被执行!');

    // 添加转发头信息
    const clientIP =
      req.socket?.remoteAddress ||
      (req.headers['x-forwarded-for'] as string) ||
      'unknown';
    proxyReq.setHeader('X-Forwarded-For', clientIP);
    proxyReq.setHeader(
      'X-Forwarded-Proto',
      req.headers['x-forwarded-proto'] || 'http',
    );
    proxyReq.setHeader('X-Forwarded-Host', req.headers.host || 'unknown');
    proxyReq.setHeader('X-Real-IP', clientIP);

    // 移除敏感头信息
    proxyReq.removeHeader('cookie');
    proxyReq.removeHeader('authorization');

    // 处理POST请求体 - 直接从原始请求流中读取

    formatBody(req)
      .then((body) => {
        const { bodyStr } = body;
        // 记录请求日志
        this.logger.log(
          `代理请求 ${req.method} ${req.url} 到 ${target}; body: ${bodyStr};`,
        );
      })
      .catch((err) => {
        this.logger.error(
          `代理请求 ${req.method} ${req.url} 到 ${target} 失败; body: ${err.message};`,
        );
      });
  },
  // 响应处理
  proxyRes: (proxyRes, req, res) => {
    // 添加自定义响应头
    proxyRes.headers['X-Proxy-By'] = 'NestJS-MiddleProxy';
    proxyRes.headers['X-Proxy-Timestamp'] = new Date().toISOString();

    // 移除敏感响应头
    delete proxyRes.headers['set-cookie'];
    delete proxyRes.headers['server'];

    formatBody(proxyRes)
      .then((body) => {
        const { bodyStr } = body;
        // 记录请求日志
        this.logger.log(
          `代理响应  ${proxyRes.statusCode} ${req.method} ${req.url} body: ${bodyStr};`,
        );
      })
      .catch((err) => {
        this.logger.error(
          `代理响应 ${proxyRes.statusCode} ${req.method} ${req.url} 失败; body: ${err.message};`,
        );
      });
  },
}
目前问题全部解决。
*还有一种方法,就是全局开启 bodyParser(不推荐)
js
  const app = await NestFactory.create<NestExpressApplication>(
    AppModule,
    new ExpressAdapter(),
    { bodyParser: true },
  );
然后再转发的时候,把被转换的body还原回去。
但是,这个还原可是一个技术活,如果有的边界情况没考虑到,就可能导致转发的数据异常,进而影响业务,这个就得不偿失了。
所以我还是建议使用上面的那种方法,虽然是麻烦,但是不会影响业务,因为formatBody 只是消费了body,并没有修改它。
最后编辑于:2026-01-11 09:11:52
©著作权归作者所有,转载或内容合作请联系作者。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,little笔记系信息发布平台,仅提供信息存储服务。