AsyncLocalStorage 适用于 NestJS Guard 的 canActivate 方法,但不适用于 Passport Strategy 的 validate 方法

问题描述 投票:0回答:1

我正在开发一个 NestJS 应用程序,我需要使用 AsyncLocalStorage 维护特定于请求的上下文。我有一个 ContextRequestService,它使用 AsyncLocalStorage 在整个请求生命周期中存储和检索上下文数据。

这是我的 ContextRequestService:

import { Injectable } from '@nestjs/common';
import { AsyncLocalStorage } from 'async_hooks';

interface ContextStore {
  lang: string;
  requestId: string;
  userId?: number;
  clinicId?: number;
}

@Injectable()
export class ContextRequestService {
  constructor(
    private readonly asyncLocalStorage: AsyncLocalStorage<ContextStore>,
  ) {}

  run(data: ContextStore, callback: () => void) {
    this.asyncLocalStorage.run(data, callback);
  }

  getLang(): string {
    const store = this.asyncLocalStorage.getStore();
    return store?.lang || 'en';
  }

  getRequestId(): string {
    const store = this.asyncLocalStorage.getStore();
    return store?.requestId || 'N/A';
  }

  setUserId(id: number) {
    const store = this.asyncLocalStorage.getStore();
    this.asyncLocalStorage.enterWith({ ...store, userId: id });
  }

  getUserId() {
    const store = this.asyncLocalStorage.getStore();
    return store?.userId;
  }

  setClinicId(id: number) {
    const store = this.asyncLocalStorage.getStore();
    this.asyncLocalStorage.enterWith({ ...store, clinicId: id });
  }

  getClinicId() {
    const store = this.asyncLocalStorage.getStore();
    return store?.clinicId;
  }
}

第一种模式:

在我的自定义 JwtAuthGuard 中,我可以毫无问题地从 AsyncLocalStorage 设置和检索数据:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from "@nestjs/passport";
import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ContextRequestService } from "../common/context/context-request";

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
  constructor(private reflector: Reflector, private readonly contextRequestService: ContextRequestService) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    this.contextRequestService.setClinicId(10)
    return super.canActivate(context);
  }
}

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ContextRequestService } from '../../common/context/context-request';
import { AsyncLocalStorage } from 'async_hooks';

interface ContextStore {
  lang: string;
  requestId: string;
  userId?: number;
  clinicId?: number;
}
@Injectable()
export class AccessGuard implements CanActivate {
  constructor(
    private readonly contextRequest: ContextRequestService,
    private readonly asyncLocalStorage: AsyncLocalStorage<ContextStore>,
    ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {

    ...
    
    console.log("____first mode_____", this.asyncLocalStorage.getStore());
    
    ...
  }
 }

第二种模式:

import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { ContextRequestService } from "../common/context/context-request";
import {Injectable} from '@nestjs/common';
import { AsyncLocalStorage } from "async_hooks";

interface ContextStore {
  lang: string;
  requestId: string;
  userId?: number;
  clinicId?: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor( private readonly contextRequestService: ContextRequestService,
    private readonly asyncLocalStorage: AsyncLocalStorage<ContextStore>) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }
  validate = async (payload: { userId: string, clinicId: number }) => {
    this.contextRequestService.setUserId(parseInt(payload.userId));
    this.contextRequestService.setClinicId(1000000);
    console.log("______ the second mode__ JWT Strategy__", this.asyncLocalStorage.getStore());
    return { userId: payload.userId };
  }
}

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ContextRequestService } from '../../common/context/context-request';
import { AsyncLocalStorage } from 'async_hooks';

interface ContextStore {
  lang: string;
  requestId: string;
  userId?: number;
  clinicId?: number;
}
@Injectable()
export class AccessGuard implements CanActivate {
  constructor(
    private readonly asyncLocalStorage: AsyncLocalStorage<ContextStore>,
    ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {

    ...
    
    console.log("____second mode_____AccessGuard ", this.asyncLocalStorage.getStore());
    ...

  }
 }

问题

为什么 AsyncLocalStorage 在我的守卫的 canActivate 方法中工作,但在我的 Passport 策略的 validate 方法中不起作用?

输出第一模式:

____first mode_____ {
  lang: 'en',
  requestId: '0e7fb339-7850-410f-a46a-d40e51bbb52e',
  clinicId: 10
}

输出第二模式:

______ the second mode__ JWT Strategy__ {
  lang: 'en',
  requestId: '2593cef0-407d-42bc-9d88-dfb1c8d657c5',
  clinicId: 1000000,
  userId: 1
}
____second mode_____AccessGuard  {
  lang: 'en',
  requestId: '2593cef0-407d-42bc-9d88-dfb1c8d657c5'
}

node.js nestjs nestjs-passport async-hooks
1个回答
0
投票

简短的回答,不要使用

.enterWith
和扩展运算符,除非您绝对确定您(以及您正在使用的所有节点模块)在该请求的上下文中没有在任何地方执行
.then
操作。这两件事设计并不总是兼容的。


长答案(过于简单化):

如果您使用 AsyncLocalStorage,每次创建 Promise 时,AsyncLocalStorage 都会创建一个指向存储的 new 指针并给出您的 Promise THAT。然后,它获取该指针的当前值并将其放回存储中。

这不是代码,但它绘制了图片:

let globalStore = { requestId: 1 }
;() => {
  let innerStore = globalStore
  await doPromise()
  globalStore = innerStore
})()

当您执行

.enterWith
时,它会将存储指针替换为您给它的值:

innerStore = { ...innerStore } // and spread overwrites the pointer, passing by values

现在突然出现这段代码:

const store = this.asyncLocalStorage.getStore();
this.asyncLocalStorage.enterWith({ ...store, userId: id });

不再与此代码相同:

const store = this.asyncLocalStorage.getStore();
store.userId = id;

如果任何地方的任何代码使用

.then
,那么您同时创建了两个两个 Promise,并且您知道它创建 Promise 时会做什么...这里是
.enterWith

let globalStore = { requestId: 1 }
// let promise1 = first()
let innerStore1 = globalStore
// promise2 = promise1.then(second)
let innerStore2 = globalStore
// executing first
innerStore1 = { ...innerStore1, key1: value1 } // override the reference with values
// executing second
innerStore2 = { ...innerStore2, key2: value2 } // override the reference with values
// await promise1
globalStore = innerStore1 // values from innerStore1, which does not include key2
// await promise2
globalStore = innerStore2 // values from innerStore2, which does not include key1

但是如果您在单例存储上设置属性而不是使用

.enterWith
为其提供新对象:

let globalStore = { requestId: 1 }
// let promise1 = first()
let innerStore1 = globalStore
// promise2 = promise1.then(second)
let innerStore2 = globalStore
// executing first
innerStore1.key1 = value1 // updated the reference
// executing second
innerStore2.key2 = value2 // updated the reference
// await promise1
globalStore = innerStore1 // same reference
// await promise2
globalStore = innerStore2 // same reference

如果您使用

.enterWith
,但使用
await
而不是
.then
来链接您的承诺:

let globalStore = { requestId: 1 }

// await first()
let innerStore1 = globalStore
innerStore1 = { ...innerStore1, key1: value1 }
globalStore = innerStore1 // includes key1

// await second()
let innerStore2 = globalStore // includes key1
innerStore2 = { ...innerStore2, key2: value2 }
globalStore = innerStore2 // includes key1 & key2

所以这是你的固定

ContextRequestService

export class ContextRequestService {
  constructor(
    private readonly asyncLocalStorage: AsyncLocalStorage<ContextStore>,
  ) {}

  run(data: ContextStore, callback: () => void) {
    this.asyncLocalStorage.run(data, callback);
  }

  getLang(): string {
    const store = this.asyncLocalStorage.getStore();
    return store?.lang || 'en';
  }

  getRequestId(): string {
    const store = this.asyncLocalStorage.getStore();
    return store?.requestId || 'N/A';
  }

  setUserId(id: number) {
    const store = this.asyncLocalStorage.getStore();
    store.userId = id;
  }

  getUserId() {
    const store = this.asyncLocalStorage.getStore();
    return store?.userId;
  }

  setClinicId(id: number) {
    const store = this.asyncLocalStorage.getStore();
    store.clinicId = id;
  }

  getClinicId() {
    const store = this.asyncLocalStorage.getStore();
    return store?.clinicId;
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.