在Nest.js中访问Stripe Webhook的原始主体

40

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

按照示例,我将以下内容添加到模块中,该模块具有需要原始正文的控制器方法。

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,但结果几乎相同。在这种情况下,req.readable 总是为 false,所以我也被卡住了。
我猜这是 Nest.js 的问题,但我不确定...

在创建NestApplicationbootstrap方法中,您可能没有禁用Nest的默认bodyParser - kaznovac
11个回答

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

json-body.middleware.ts

import type { 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 type { 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:

main.ts

[...]

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

[...]

顺便说一句,`req.rawbody` 在很久以前就从 `express` 中移除了。

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


这是我认为使用 Nest 最好的解决方案,应该被接受的答案。 - vsiguero
13
这是一个很好的回答,但请记住调整nestjs的初始化以关闭bodyParser:const app = await NestFactory.create(AppModule, { bodyParser: false, }) - Krzysztof Kaczor
1
我认为你应该在JsonBodyMiddleware中调用.exclude({ path: '/stripe-webhooks', method: RequestMethod.POST, })。 - Mu-Majid
6
Body parser已经过时,现在在express中提供了jsonraw两个选项。例如:import { Request, Response, raw, json } from 'express'; - Ryall
const app = .... 之后,您应该调用 await app.init() - Jan Jarčík
显示剩余5条评论

37

昨晚我遇到了类似的问题,试图验证一个Slack token。

我们最终使用的解决方法需要禁用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();
}

继续闪耀!

编辑:

自从这个问题被提出以来,Nest.js已经实现了这个用例。现在你可以按照以下步骤获取原始内容:

main.js

const app = await NestFactory.create(AppModule, { rawBody: true });

然后在你的控制器中:

 @Post()
 webhook(@Req() req: RawBodyRequest<Request>) { 
  const rawBody = req.rawBody;
 }

点击此处阅读更多内容。


1
注意:NestJS的嵌入式rawBody与其他JSON body解析参数不兼容,例如更改“limit”(例如为'50mb')。就我所知,你最初的回答对我更有效。 - Antoine OL
1
我尝试使用 rawBody: true 功能获取二进制图像,但出于某些原因它无法工作。有任何想法为什么会这样吗? - MegaSpaceHamlet
Nest.js 刚刚在这个部分进行了更新:现在你可以通过使用 .useBodyParser 指定 body-parser 选项来混合像 limit 等的 body-parser 选项与 rawBody。这里有一个示例 - Luxior

24

今天,我正在使用NestJS和Stripe。

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

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

并且它将被限制在这条路线上!不允许超载。


1
@a7md0,我可以在控制器钩子上获取“原始”数据,请您分享更多细节。 - zulqarnain
@zulqarnain 对于Stripe,我只是将原始数据(Buffer类型)传递给了eventConstructor的主体。 - a7md0
1
这似乎是最简单的解决方案。还要记得正确导入它: import * as bodyParser from 'body-parser' - Eliezer Steinbock
很好的解决方案,而且也有效。不过现代实现中最好使用 import { raw } from 'express';raw({ type: 'application/json' }) - Ryall
1
如果您配置了多个解析器,请确保更具体的解析器出现在通用解析器之前。 app.use('/payment/hooks', bodyParser.raw({type: 'application/json'})); app.use(bodyParser.json()); - andre
显示剩余2条评论

10

2022年第三季度更新

现在可以通过专用的 rawBody 选项轻松实现,无需额外操作: https://docs.nestjs.com/faq/raw-body

p.s. 只需不要忘记将您的nest依赖更新到最新版本:

npm update @nestjs/core
npm update @nestjs/common
npm update @nestjs/common
npm update @nestjs/platform-express //if you are using express

由于某些原因,这对我来说不起作用。 - Jeremiah
1
@Jeremiah 如果您提供更多的上下文,我可以帮助您。 - chakzefir
它起作用了。但我必须部署它进行测试...我无法在本地测试,但它可以工作。谢谢。 - Jeremiah
2
@Jeremiah,我在本地也遇到了问题。我的问题是我使用了设置的Webhook提供的签名密钥,但如果您使用CLI转发事件,则需要在调用stripe listen时使用终端中提供的密钥。 - cbronson

6

我用一行代码解决了这个问题 :)

main.ts

import * as express from 'express';

async function bootstrap() {
...
  app.use('/your-stripe-webhook', express.raw({ type: "*/*" })); // <- add this!
...
  await app.listen(8080)
}

...不需要添加任何中间件。 ...无需禁用bodyParser


1
在NestJS v.7.1.1上运行得非常好。 - undefined

3

我发现由于某些原因,请求体解析程序未能将请求传递给链中的下一个处理函数。

NestJS已经支持当请求类型为"text/plain"时的原始请求数据,我的解决方案是:

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();
  }
}

1
这个运行得非常顺利。 - Daniel

3
你可以使用其中一个花哨的答案,或者只需继续阅读NestJS官方文档并使用:
const app = await NestFactory.create(AppModule, {
  rawBody: true,
  bodyParser: true,
  ...

而且关于你的路线定义:
  @Public()
  @Post("webhooks")
  async createStripeWebhookAction(
    @Req() req: RawBodyRequest<Request>,
    @Res() res: Response,
  ) {
  //... do stuff with it

运作得非常顺利。

1
这对我来说似乎也很有效,并且也是最简单的方法。 - undefined

1

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)
    }
}

将`bodyParser`选项设置为`false`以在bootstrap上禁用。
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()

"Refs:" (参考资料:)

1
这是我对在NestJS处理程序中获取原始(文本)正文的看法:
  1. 按照JSDoc示例配置应用程序,使用preserveRawBodyInRequest(仅限于stripe webhook,请使用"stripe-signature"作为过滤器标头)
  2. 在处理程序中使用RawBody装饰器来检索原始(文本)正文

raw-request.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,
      },
    ),
  );
}

0
我为这个问题创建了一个简单的中间件路由器:

express-middleware-router.ts

import { NextFunction, Request, Response } from 'express';

export type NextHandleFunction = (req: Request, res: Response, next: NextFunction) => void;

export interface MiddlewareRoute {
    /**
     * Exact match with `request.originalUrl`. Optionally matches via
     * `request.originalUrl.startsWith` when ending with a `*`.
     */
    path: string;
    middleware: NextHandleFunction;
}

/**
 * Runs middleware if a route is matching `request.originalUrl`.
 * @param routes Order of routes is important. When using a catch all route like
 * `'*'`, make sure it is the last in the array.
 */
export function middlewareRouter(routes: MiddlewareRoute[]) {
    return (req: Request, res: Response, next: NextFunction) => {
        const nextMiddleware = routes.reduce((prev, curr) => {
            if (prev) {
                return prev;
            }

            const isMatch = curr.path.endsWith('*')
                ? req.originalUrl.startsWith(curr.path.slice(0, -1))
                : req.originalUrl === curr.path;

            return isMatch ? curr : prev;
        }, undefined) as MiddlewareRoute | undefined;
        nextMiddleware ? nextMiddleware.middleware(req, res, next) : next();
    };
}

它可以像这样使用:

main.ts

import { MiddlewareRoute, middlewareRouter } from './express-middleware-router';

const middlewareRoutes: MiddlewareRoute[] = [
    {
        path: '/stripe',
        middleware: text({ type: '*/*' }),
    },
    {
        path: '/high-json-limit/*',
        middleware: json({ limit: '10mb' }),
    },
    {
        path: '*',
        middleware: json(),
    },
];

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

app.use(middlewareRouter(middlewareRoutes));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接