【问题标题】:NestJS Interceptors: Unable to set HTTP Headers on outgoing requestsNestJS 拦截器:无法在传出请求上设置 HTTP 标头
【发布时间】:2020-07-03 15:37:09
【问题描述】:

我正在用 NestJS 编写具有一组通用标头的 API。我决定使用拦截器将标头附加到传出请求。标头没有附加到请求中,因此请求不断失败。

拦截器

import * as utils from '../utils/utils';
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor
} from '@nestjs/common';
import { HEADERS } from '../middlewares/headers.constant';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { DATA_PARTITION_ID } from '../app.constants';

@Injectable()
export class HeadersInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<void> {
    const ctx = context.switchToHttp();
    const request: Request = ctx.getRequest();

    this.setHeaders(request);

    return next.handle();
  }

  private setHeaders(request): void {
    this.updateHeaders(request, HEADERS.ACCEPT, 'application/json');
    this.updateHeaders(request, HEADERS.CONTENT_TYPE, 'application/json');
    this.updateHeaders(request, HEADERS.ACCEPT_ENCODING, 'gzip, deflate, br');
    this.updateHeaders(
      request,
      HEADERS.DATA_PARTITION_ID,
      DATA_PARTITION_ID
    );
    this.updateHeaders(
      request,
      HEADERS.AUTHORIZATION,
      `Bearer ${utils.parseCookies(request).stoken}`
    );
    this.updateHeaders(request, HEADERS.APP_KEY, '');
  }

  private updateHeaders(
    request: Request,
    property: string,
    value: string
  ): void {
    if (!request.headers.hasOwnProperty(property)) {
      request.headers[property] = value;
    } else {
      void 0;
    }
  }
}

这个拦截器只做一件事:访问请求并附加标头并将控制权传递给下一个处理程序。

枚举

export enum HEADERS {
  DATA_PARTITION_ID = 'Data-Partition-Id',
  AUTHORIZATION = 'Authorization',
  CONTENT_TYPE = 'Content-Type',
  APP_KEY = 'appkey',
  ACCEPT = 'accept',
  ACCEPT_ENCODING = 'accept-encoding'
}

控制器

import { Body, Controller, Post, Req, UseInterceptors } from '@nestjs/common';
import { HeadersInterceptor } from '../interceptors/headers.interceptor';
import { SearchData } from './models/search-data.model';
import { SearchResults } from './models/search-results.model';
import { SearchService } from './search.service';

@Controller('')
@UseInterceptors(new HeadersInterceptor())
export class SearchController {
  constructor(private searchService: SearchService) {}

  @Post('api/search')
  async searchDataById(@Body() searchData: SearchData, @Req() req): Promise<SearchResults> {
    console.log(req.headers);
    return await this.searchService.getSearchResultsById(searchData);
  }
}

服务

import { HttpService, HttpStatus, Injectable } from '@nestjs/common';
import { AppConfigService } from '../app-config/app-config.service';
import { DataMappingPayload } from './models/data-mapping-payload.model';
import { SearchData } from './models/search-data.model';
import { SearchModelMapper } from './search.service.modelmapper';
import { SearchResults } from './models/search-results.model';
import { ServiceException } from '../exception/service.exception';

@Injectable()
export class SearchService {
  constructor(
    private searchModelMapper: SearchModelMapper,
    private configService: AppConfigService,
    private readonly httpService: HttpService
  ) {}

  async getSearchResultsById(searchData: SearchData): Promise<SearchResults> {
    if (searchData.filters.collectionId) {
      console.log(this.configService.appConfig.urls.SEARCH_RESULTS_BY_COLLECTION_ID_URL.replace(
          '${collectionId}',
          searchData.filters.collectionId
        )
      );
      const searchResultsAPI = await this.httpService
        .get(
          this.configService.appConfig.urls.SEARCH_RESULTS_BY_COLLECTION_ID_URL.replace(
            '${collectionId}',
            searchData.filters.collectionId
          )
        )
        .toPromise();
      const kinds = this.searchModelMapper.getUniqueKinds(
        searchResultsAPI.data.results
      );
      const mappingPayload = await this.getDataMapping(kinds);
      return this.searchModelMapper.generateSearchResults(
        kinds,
        mappingPayload,
        searchResultsAPI.data.results
      );
    } else {
      this.raiseException();
    }
  }

  async getDataMapping(kinds: string[]): Promise<[]> {
    const entityKindNames: DataMappingPayload = {
      entityKindNames: kinds
    };
    const dataMappingAPI = await this.httpService
      .post(
        this.configService.appConfig.urls.DATA_CATALOG_SERVICE_URL,
        JSON.stringify(entityKindNames)
      )
      .toPromise();

    return dataMappingAPI.data.entityViewData;
  }

  // To be moved to util functions
  private raiseException(): void {
    throw new ServiceException(
      {
        message: 'This does not have a collection id',
        missing: 'Collection Id',
        code: HttpStatus.BAD_REQUEST
      },
      HttpStatus.BAD_REQUEST
    );
  }
}

当我在 控制器 中访问 req.headers 时,我确实获得了我需要通过拦截器设置的所有标头。

{
[0]   'accept-encoding': 'gzip, deflate, br',
[0]   'accept-language': 'en-US,en;q=0.9',
[0]   cookie: '_ga=GA1.2.1433024000.1564057108; wfx_unq=AL2gejqqEGELJ5FQ; trafficManagerV2Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTJOakU0T0RJNE1nP
T0ifQ.eyJwZXJtaXRVcmwiOiJodHRwczovL2V2ZC5kYXRhLmRlbGZpLmNsb3VkLnNsYi1kcy5jb20vIiwiY291bnRyeUNvZGUiOiJJTiIsImlzcyI6ImNmcy10cmFmZmljLW1hbmFnZXIiLCJpYXQiOjE1NjYyNzM3ND
gsImV4cCI6MTU2NjI4ODE0OCwiYXVkIjoiaHR0cHM6Ly9ldmQuZGF0YS5kZWxmaS5jbG91ZC5zbGItZHMuY29tLyJ9.QHvZGR4DXdGpsWWNCnypPFttaBlpBCBSvy2N_Z0mgSD6W86g4f61GhO2XzFyIm7P20qAjkXHl
3CIo8R66wtYQqMIAOEd2BPcJVnKg9vdt2kxd1Fhk66BWTFd_xtTdyEgcwMuCmEkYEeFK1_cXrlbeGYpaRiXD6w6K1_2U1Wxtbu82BNp7R4eAuiLRbbLBdsuPgLwXsOI8YpFTMdpiUDMnZnTfw-Fr2F93KMzHKTswLy0y
QZVPtONj8BwXDPf15s2vLiTyzgof4ByM7O_eBIbBDse5tFufBXFABnr709Oi6AKUGMeVKsgwCo1d1Yxs7MR6nbNmyG3rFxKzhk5Xxehzw; x-origin-country=IN; stoken=eyJ0eXAiOiJKV1QiLCJhbGciOiJSU
zI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJzdWIiOiJycHJhYmh1N0BzbGIuY29tIiwiaXNzIjoic2F1dGgtcHJldmlldy5zbGIuY29tIiwiYXVkIjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC
5jb20iLCJpYXQiOjE1ODQ5Mzk3MjYsImV4cCI6MTU4NTAyNjEyNiwicHJvdmlkZXIiOiJzbGIuY29tIiwiY2xpZW50IjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJ1c2VyaWQiOiJycHJhYmh1N0
BzbGIuY29tIiwiZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwiYXV0aHoiOiIiLCJsYXN0bmFtZSI6IlByYWJodSIsImZpcnN0bmFtZSI6IlJ1c2hpa2VzaCBTdWJoYXNoIiwiY291bnRyeSI6IiIsImNvbXBhbnkiOi
IiLCJqb2J0aXRsZSI6IiIsInN1YmlkIjoiRjBfSUMxSjl4SHBaSGVUbnVBaWRCYVhtdzI1YmxuOUhYSXIwMnNscW8wTSIsImlkcCI6Im8zNjUiLCJoZCI6InNsYi5jb20iLCJkZXNpZCI6InJwcmFiaHU3LXNsYi1jb2
0tNWZkODc5NzZAZGVzaWQuZGVsZmkuc2xiLmNvbSIsImNvbnRhY3RfZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwicnRfaGFzaCI6IlAzUG1yRXd5WExCR1VwTi05TTdybEEifQ.Z61iRRoS7J1IpF_V_rWLcrgeaSf
QyZG3K5vU4jps_LqB3VkPSvjHXLdv7Ga_LLPI_v2J-WFityHVBnYxLEzKmOuNc_jToPwmBqCmLLfSzIFGiJrFKby09ZbVoCCLHxjyUwB_Uc2VmWuYLce7oPpVFxelgRqnRjO3ymlPm65OvrR09fHiOlo52TULwbyyzeg
xzfodkl0eVTM7TURDi1RxGNHvw8Ghxt--AVIcgCT7hBDxA6w11D7Cr6fWBp1VpE2yawTESUWtZJn5tBmMZeZq2QobptNcuFdiAstQpvi_B5MqY1HY5LjVLOb2jAnEoCTl_gmEfyWr_aIKAFioK4YcQQ; _gid=GA1.2.
1341318697.1566283218; account-id=tenant1; _gat=1; traffic-manager-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJwZXJtaXRVcmwi
OiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJjb3VudHJ5Q29kZSI6IklOIiwiaXNzIjoiY2ZzLXRyYWZmaWMtbWFuYWdlciIsImlhdCI6MTU4NDkzMTk5NywiZXhwIjoxNTg0OTQ2Mzk3LCJhdWQiOiJodHRwOi8vbG9j
YWxob3N0OjgwODAifQ.uVs2Uuy_Okzn0t3GPESH7cCR4OAb_ISr160JrydaKfkHogaKsuNEa7BI1vgQY8uywYle2P_sRaYT_FaoR9cF2iqHH7R7YHVdKEdNm_Gb2ji8nnLMjXORAMB78YtHt4SvnCNYrAxTqRPVhxRot
dQc6dcrVgzkxKxedDvnZTR81DfoOa00oeKrU7X62MSGMRDmz7TYLNxbaw0viJ-MlJ2AMHs_YhyRSHvmmG_5d0TVfNLBSnAiXlTH06iigVXfT5v-BbRukJJzaW1Pj30fde2G2ni0SZ8sK6nlrpu_0Tlu5-v1dKmdofhBs
qC8y8sCjZ8fTw4yZICl5AwPGZ4IOLkAeg',
[0]   'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
[0]   'content-type': 'application/json',
[0]   accept: 'application/json',
[0]   appkey: '',
[0]   'cache-control': 'no-cache',
[0]   'postman-token': 'cb397012-71aa-460a-b66b-28600538faf9',
[0]   host: 'localhost:8080',
[0]   'content-length': '77',
[0]   connection: 'keep-alive',
[0]   'Data-Partition-Id': 'tenant1',
[0]   Authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJzdWIiOiJycHJhYmh1N0BzbGIuY29tIiwiaXNzIjoic2F1dGgtcHJldmlldy5z
bGIuY29tIiwiYXVkIjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJpYXQiOjE1ODQ5Mzk3MjYsImV4cCI6MTU4NTAyNjEyNiwicHJvdmlkZXIiOiJzbGIuY29tIiwiY2xpZW50IjoidGVzdC1zbGJk
ZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJ1c2VyaWQiOiJycHJhYmh1N0BzbGIuY29tIiwiZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwiYXV0aHoiOiIiLCJsYXN0bmFtZSI6IlByYWJodSIsImZpcnN0bmFtZSI6
IlJ1c2hpa2VzaCBTdWJoYXNoIiwiY291bnRyeSI6IiIsImNvbXBhbnkiOiIiLCJqb2J0aXRsZSI6IiIsInN1YmlkIjoiRjBfSUMxSjl4SHBaSGVUbnVBaWRCYVhtdzI1YmxuOUhYSXIwMnNscW8wTSIsImlkcCI6Im8z
NjUiLCJoZCI6InNsYi5jb20iLCJkZXNpZCI6InJwcmFiaHU3LXNsYi1jb20tNWZkODc5NzZAZGVzaWQuZGVsZmkuc2xiLmNvbSIsImNvbnRhY3RfZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwicnRfaGFzaCI6IlAz
UG1yRXd5WExCR1VwTi05TTdybEEifQ.Z61iRRoS7J1IpF_V_rWLcrgeaSfQyZG3K5vU4jps_LqB3VkPSvjHXLdv7Ga_LLPI_v2J-WFityHVBnYxLEzKmOuNc_jToPwmBqCmLLfSzIFGiJrFKby09ZbVoCCLHxjyUwB_U
c2VmWuYLce7oPpVFxelgRqnRjO3ymlPm65OvrR09fHiOlo52TULwbyyzegxzfodkl0eVTM7TURDi1RxGNHvw8Ghxt--AVIcgCT7hBDxA6w11D7Cr6fWBp1VpE2yawTESUWtZJn5tBmMZeZq2QobptNcuFdiAstQpvi_B
5MqY1HY5LjVLOb2jAnEoCTl_gmEfyWr_aIKAFioK4YcQQ'
[0] }

当我检查实际请求的日志时,它说 Authorizationnull。这意味着请求不会被拦截并且不会附加标头。

有没有人遇到过类似的问题?

【问题讨论】:

  • outgoing request 是指要发送回客户端的响应吗?
  • 不,我的意思是在访问服务器之前。 (this.httpservice.get(url, headers)
  • 服务文件。在 get 方法中,我希望附加标题。

标签: node.js typescript express interceptor nestjs


【解决方案1】:

我认为以上任何内容都不能回答如何从 nest.js api 请求中读取 cookie/header 并将其包含在所有传出请求中的问题(假设我们从这个 api 调用另一个 api 而不是this api) 。第二部分如何设置标头已回答,但这里的挑战是在 HttpModule.register 或 onModuleInit() 中获取 cookie 和标头。

【讨论】:

    【解决方案2】:

    我使用 nest-keycloak-connect 依赖项来管理我的端点的 JWT 验证。我正在以微服务方式开发端点。 nestjs 后端需要调用由同一个 Keycloak 保护的另一个 API。所以我需要从传入的 http 请求中传递相同的令牌。 TLDR。

    解决方法很简单:

    1. 通过实现CanActivate 接口添加保护(我尝试使用拦截器但没有成功

    2. 从header中提取JWT,放到axios默认header中

      axios.defaults.headers.common['Authorization'] = 'Bearer' + ${token[1];

    3. 在应用模块提供程序中添加该保护。

    app.module.ts:

        {
          provide: APP_GUARD,
          useClass: JwtAuthGuard,
        },
        // {
        //   provide: APP_INTERCEPTOR,
        //   useClass: TokenInterceptor,
        // },
    

    jwt.auth.guard.ts:

    import { ExecutionContext, Injectable } from '@nestjs/common';
    import axios from 'axios';
    import { CanActivate } from '@nestjs/common';
    
    @Injectable()
    export class JwtAuthGuard implements CanActivate {
      canActivate(context: ExecutionContext) {
        const ctx = context.switchToHttp();
        const request = ctx.getRequest<Request>();
        const authorization = request.headers['authorization'];
    
        if (authorization !== '') {
          const token = authorization.split(' ');
          //console.info('JwtAuthGuard token', token[1]);
          axios.defaults.headers.common['Authorization'] = `Bearer ${token[1]}`;
        }
        
        return true;
      }
    }
    

    client.ts:

    import { HttpService, Inject, Injectable } from '@nestjs/common';
    import { ConfigType } from '@nestjs/config';
    import { AxiosResponse } from 'axios';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    import coreConfig from 'src/environment/core.config';
    import axios from 'axios';
    
    @Injectable()
    export class Client {
      constructor(
        private readonly http: HttpService,
      ) {}
    
      findAll(): Observable<Array<string>> {
        console.info('axios.defaults.headers', axios.defaults.headers);
        const url = 'http://localhost:3000/api2/test';
        console.info('calling url', url);
        return this.http.get(url).pipe(
          map((axiosResponse: AxiosResponse) => {
            return axiosResponse.data;
          }),
        );
      }
    }
    

    test.controller.ts:

    import { Controller, Get } from '@nestjs/common';    
    import { AllowAnyRole } from 'nest-keycloak-connect';
    import { Observable, of } from 'rxjs';
    
    @Controller('api')
    export class TestController {
      @AllowAnyRole()
      @Get()
      findAll(): Observable<Array<string>> {
        console.info('api find all');
        return of(['today','is', 'sunny']);
      }
    }
    

    【讨论】:

      【解决方案3】:

      我只想在这里分享我的经验,如何根据正在进行的请求添加自定义标头

      import { REQUEST } from '@nestjs/core';
      import { Module, HttpModule } from '@nestjs/common';
      
      @Module({
          imports: [HttpModule.registerAsync({
              useFactory: request => {
                  let automated = 0;
                  if (request.get('host').includes('localhost')) {
                      automated = 1;
                  }
                  return { headers: { automated } };
              },
              inject: [REQUEST],
          })],
      })
      

      我使用了自定义提供程序并注入了请求,因此我可以根据给定的请求确定我将设置哪些标头,这将很有帮助,因为我们可以根据给定的请求动态设置任何标头,而无需手动设置它每个 axios 请求。

      并根据NestJS Documentation使用它

      @Injectable()
      export class CatsService {
          constructor(private httpService: HttpService) {}
      
          findAll(): Observable<AxiosResponse<Cat[]>> {
              return this.httpService.get('http://localhost:3000/cats');
          }
      }
      

      【讨论】:

      • 这用于配置 Axios,其中要设置的标头取决于传入的请求。我一直觉得应该有更好的方法来做到这一点,但我还没有找到。
      • useFactory: 请求是旧的 api?根据文档 - useFactory: async (configService: ConfigService) => ({
      【解决方案4】:

      如果外部 HTTP 调用始终需要标头,您可以直接在 nestJs 中添加一个 Axios Interceptor HttpService,就像他在此 post 中所做的一样,以记录他的请求。

      重要的部分是:

      1. 让你的app.module.ts 实现OnModuleInit
      2. 添加方法onModuleInit()
      3. onModuleInit() 中添加this.httpService.axiosRef.interceptors.request.use(functionThatWillAddHeadersToRequest(config));

      config 包含请求所需的所有信息,包括标头。

      现在,您所有使用 HttpService 的传出请求都应该包含您的 HTTP 标头。

      Axios interceptors github

      【讨论】:

        【解决方案5】:

        如果我对您的理解正确,您希望将标头添加到来自 HttpService 的传出 HTTP 调用中。 NestJS 中的interceptor 适用于IncomingMessage(通常是传入请求)和ServerResponse(或通常是传出响应)。它看不到从 HttpService 或任何其他 HTTP 客户端发送的内容。相反,您需要在方法级别设置标头,如果它们都是通用值,则需要在模块级别设置。 HttpModule 有一个 registerregisterAsync 方法,可用于将值传递给每个 HttpService 调用,因此如果您有通用标头,则可以这样管理它们:

        @Module({
          imports: [
            HttpModule.register({
              headers: {} // object of headers you want to set
            }),
          ]
        })
        export class MyModule {}
        

        现在,当您使用 httpService.get(url) 时,您将使用它发送标头。

        【讨论】:

        • 感谢您回答@Jay。我有一个需要从 cookie 中读取的令牌。不知道如何在这里访问它。
        • @AnkitTanna 看到塞德里克的回答。您可以改用工厂,它允许注入请求。可以从请求中获取cookies。
        猜你喜欢
        • 1970-01-01
        • 2019-02-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-02
        • 1970-01-01
        相关资源
        最近更新 更多