我正在开发一个 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'
}
简短的回答,不要使用
.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;
}
}