【问题标题】:Access raw body of Stripe webhook in Nest.js在 Nest.js 中访问 Stripe webhook 的原始主体
【发布时间】:2019-06-18 04:16:19
【问题描述】:

我需要在我的 Nest.js 应用程序中访问来自 Stripe 的 webhook 请求的原始正文。

this 示例之后,我将以下内容添加到具有需要原始主体的控制器方法的模块中。

function addRawBody(req, res, next) {
  req.setEncoding('utf8');

  let data = '';

  req.on('data', (chunk) => {
    data += chunk;
  });

  req.on('end', () => {
    req.rawBody = data;

    next();
  });
}

export class SubscriptionModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(addRawBody)
      .forRoutes('subscriptions/stripe');
  }
}

在控制器中,我使用@Req() req然后req.rawBody 来获取原始主体。我需要原始正文,因为 Stripe api 的constructEvent 正在使用它来验证请求。

问题是请求被卡住了。似乎 req.on 既不是为数据调用,也不是为结束事件调用。所以next()在中间件中没有被调用。

我也尝试使用raw-body 就像here 一样,但我得到了几乎相同的结果。在那种情况下, req.readable 总是错误的,所以我也被困在那里。

我猜这是 Nest.js 的问题,但我不确定...

【问题讨论】:

  • bootsrap 方法中创建NestApplication 时,您可能没有禁用Nest 的默认bodyParser

标签: node.js typescript express stripe-payments nestjs


【解决方案1】:

对于任何寻求更优雅解决方案的人,请关闭 main.ts 中的 bodyParser。创建两个中间件函数,一个用于rawbody,另一个用于json-parsed-body

json-body.middleware.ts

import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class JsonBodyMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: () => any) {
        bodyParser.json()(req, res, next);
    }
}

raw-body.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';

@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: () => any) {
        bodyParser.raw({type: '*/*'})(req, res, next);
    }
}

将中间件函数应用于app.module.ts 中的适当路由。

app.module.ts

[...]

export class AppModule implements NestModule {
    public configure(consumer: MiddlewareConsumer): void {
        consumer
            .apply(RawBodyMiddleware)
            .forRoutes({
                path: '/stripe-webhooks',
                method: RequestMethod.POST,
            })
            .apply(JsonBodyMiddleware)
            .forRoutes('*');
    }
}

[...]

并调整 Nest 的初始化以关闭 bodyParser:

ma​​in.ts

[...]

const app = await NestFactory.create(AppModule, { bodyParser: false })

[...]

顺便说一句,req.rawbody 早就从 express 中删除了。

https://github.com/expressjs/express/issues/897

【讨论】:

  • 这是使用 Nest 恕我直言的最佳解决方案,应该是公认的答案。
  • 这是最好的解决方案。谢谢
  • 这是一个很好的答案,但请记住调整 nestjs 的初始化以关闭 bodyParser: const app = await NestFactory.create(AppModule, { bodyParser: false, })
  • 太棒了!一旦你关闭了内置的 bodyParser,它就会像一个魅力一样工作。可以添加到答案中吗?
  • 正文解析器已弃用。 jsonraw 现在都可以在 express 中使用。例如:import { Request, Response, raw, json } from 'express';
【解决方案2】:

我发现由于某种原因,正文解析器未能移交给链中的下一个处理程序。

当内容类型为“text/plain”时,NestJS 已经支持 raw body,所以我的解决方案是这样的:

import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response } from "express";

@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => unknown) {
    req.headers["content-type"] = "text/plain";
    next();
  }
}

【讨论】:

    【解决方案3】:

    1.

    模块上应用中间件并分配控制器。

    import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
    import { raw } from 'body-parser'
    
    import { PaymentIntentController } from './payment-intent.controller'
    import { PaymentIntentService } from './payment-intent.service'
    
    @Module({
        controllers: [PaymentIntentController],
        providers: [PaymentIntentService]
    })
    export class PaymentIntentModule implements NestModule {
        configure(consumer: MiddlewareConsumer) {
            consumer.apply(raw({ type: 'application/json' })).forRoutes(PaymentIntentController)
        }
    }
    

    2.

    bodyParser 选项为 false 在引导程序上。

    import { NestFactory } from '@nestjs/core'
    
    import { AppModule } from './module'
    
    async function bootstrap() {
        const app = await NestFactory.create(AppModule, { cors: true, bodyParser: false })
    
        await app.listen(8080)
    }
    
    bootstrap()
    

    参考:

    【讨论】:

      【解决方案4】:

      这是我对在 NestJS 的处理程序中获取原始(文本)正文的看法:

      1. 使用preserveRawBodyInRequest 配置应用程序,如 JSDoc 示例所示(限制仅条带 webhook 使用 "stripe-signature" 作为过滤器标头)
      2. 在处理程序中使用 RawBody 装饰器来检索原始(文本)正文

      原始请求.decorator.ts:

      import { createParamDecorator, ExecutionContext } from '@nestjs/common';
      import { NestExpressApplication } from "@nestjs/platform-express";
      
      import { json, urlencoded } from "express";
      import type { Request } from "express";
      import type http from "http";
      
      export const HTTP_REQUEST_RAW_BODY = "rawBody";
      
      /**
       * make sure you configure the nest app with <code>preserveRawBodyInRequest</code>
       * @example
       * webhook(@RawBody() rawBody: string): Record<string, unknown> {
       *   return { received: true };
       * }
       * @see preserveRawBodyInRequest
       */
      export const RawBody = createParamDecorator(
        async (data: unknown, context: ExecutionContext) => {
          const request = context
            .switchToHttp()
            .getRequest<Request>()
          ;
      
          if (!(HTTP_REQUEST_RAW_BODY in request)) {
            throw new Error(
              `RawBody not preserved for request in handler: ${context.getClass().name}::${context.getHandler().name}`,
            );
          }
      
          const rawBody = request[HTTP_REQUEST_RAW_BODY];
      
          return rawBody;
        },
      );
      
      /**
       * @example
       * const app = await NestFactory.create<NestExpressApplication>(
       *   AppModule,
       *   {
       *     bodyParser: false, // it is prerequisite to disable nest's default body parser
       *   },
       * );
       * preserveRawBodyInRequest(
       *   app,
       *   "signature-header",
       * );
       * @param app
       * @param ifRequestContainsHeader
       */
      export function preserveRawBodyInRequest(
        app: NestExpressApplication,
        ...ifRequestContainsHeader: string[]
      ): void {
        const rawBodyBuffer = (
          req: http.IncomingMessage,
          res: http.ServerResponse,
          buf: Buffer,
        ): void => {
          if (
            buf?.length
            && (ifRequestContainsHeader.length === 0
              || ifRequestContainsHeader.some(filterHeader => req.headers[filterHeader])
            )
          ) {
            req[HTTP_REQUEST_RAW_BODY] = buf.toString("utf8");
          }
        };
      
        app.use(
          urlencoded(
            {
              verify: rawBodyBuffer,
              extended: true,
            },
          ),
        );
        app.use(
          json(
            {
              verify: rawBodyBuffer,
            },
          ),
        );
      }
      

      【讨论】:

        【解决方案5】:

        今天,

        因为我正在使用 NestJS 和 Stripe

        我安装了 body-parser (npm), 然后在 main.ts 中, 只需添加

         app.use('/payment/hooks', bodyParser.raw({type: 'application/json'}));
        

        它将被限制在这条路线上!没有过载

        【讨论】:

        • 在 hooks 的控制器中会是这样的handleWebhook(@Body() raw: Buffer)
        • @a7md0 我可以在控制器挂钩上获得raw 数据,您能否分享更多详细信息
        • @zulqarnain 对于条纹,我只是将原始类型的 Buffer 传递给 eventConstructor 正文
        • 这似乎是最简单的解决方案。还要记得正确导入:import * as bodyParser from 'body-parser'
        • 很好的解决方案,而且很有效。更好地使用 import { raw } from 'express';raw({ type: 'application/json' }) 尽管与现代实现。
        【解决方案6】:

        我昨晚在尝试验证 Slack 令牌时遇到了类似的问题。

        我们最终使用的解决方案确实需要从核心 Nest 应用程序中禁用 bodyParser,然后在使用原始请求正文向请求添加新的 rawBody 键后重新启用它。

            const app = await NestFactory.create(AppModule, {
                bodyParser: false
            });
        
            const rawBodyBuffer = (req, res, buf, encoding) => {
                if (buf && buf.length) {
                    req.rawBody = buf.toString(encoding || 'utf8');
                }
            };
        
            app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true }));
            app.use(bodyParser.json({ verify: rawBodyBuffer }));
        
        

        然后在我的中间件中我可以像这样访问它:

        const isVerified = (req) => {
            const signature = req.headers['x-slack-signature'];
            const timestamp = req.headers['x-slack-request-timestamp'];
            const hmac = crypto.createHmac('sha256', 'somekey');
            const [version, hash] = signature.split('=');
        
            // Check if the timestamp is too old
            // tslint:disable-next-line:no-bitwise
            const fiveMinutesAgo = ~~(Date.now() / 1000) - (60 * 5);
            if (timestamp < fiveMinutesAgo) { return false; }
        
            hmac.update(`${version}:${timestamp}:${req.rawBody}`);
        
            // check that the request signature matches expected value
            return timingSafeCompare(hmac.digest('hex'), hash);
        };
        
        export async function slackTokenAuthentication(req, res, next) {
            if (!isVerified(req)) {
                next(new HttpException('Not Authorized Slack', HttpStatus.FORBIDDEN));
            }
            next();
        }
        

        闪耀吧!

        【讨论】:

          猜你喜欢
          • 2021-12-26
          • 2021-10-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-08-13
          • 2020-01-21
          • 2021-08-25
          相关资源
          最近更新 更多