在Nest.js中访问Stripe Webhook的原始正文

时间:2019-01-24 12:14:39

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

我需要在我的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()在中间件中没有被调用。

我也确实尝试像here一样使用raw-body,但是得到的结果几乎相同。在这种情况下,req。可读性总是为假,所以我也被困在那里。

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

7 个答案:

答案 0 :(得分:9)

今天

我正在使用NestJS和Stripe

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

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

,它将仅限于此路线!没有过载

答案 1 :(得分:3)

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

我们最终使用的解决方案确实需要从核心Nest App禁用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();
}

发光!

答案 2 :(得分:0)

对于任何寻求更优雅解决方案的人,请关闭bodyParser中的main.ts

创建2个middlewares,一个用于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

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

顺便说一句req.rawbody已从express的后背移除-https://github.com/expressjs/express/issues/897

答案 3 :(得分:0)

这是我在NestJS的处理程序中获取raw(text)body的观点:

  1. 使用JSDoc示例中所示的preserveRawBodyInRequest配置应用程序(仅使用Stripe Webhook限制使用"stripe-signature"作为过滤器头)
  2. 在处理程序中使用RawBody装饰器来检索raw(text)body

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

答案 4 :(得分:0)

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

参考:

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:-2)

这是因为默认情况下使用bodyParser中间件,并且在中间件启动时已经消耗了主体。您可以在bodyParser中关闭main.ts

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

不过,您应该注意,在大多数其他情况下,您可能希望使用bodyParser.json,因此请将其添加到所有其他路由中。您可以使用否定的正则表达式从中间件中排除一条特定的路由,例如this thread