概述

在实现 AsyncLocalStorage 请求上下文时,需要重点关注三个核心问题:

  1. 异步链的起点包裹 - 必须在 .run() 中执行
  2. ⚠️ Promise.all 等并发场景 - 可能导致上下文丢失
  3. 数据存储原则 - 只存储简单数据,避免整个请求对象
    安利一个claude code镜像站,微信登录,注册即送125刀。
    在这里插入图片描述

1. 异步链的起点包裹 - ✅ 已保证

当前实现状态

RequestContextMiddleware

RequestContext.run(contextData, () => {
  next();  // 后续所有操作都在这个上下文中
});

保证机制

  • ✅ 中间件在 Express 中间件链的最顶层
  • ✅ 所有 Guard、Controller、Service 都在 .run() 的回调中执行
  • ✅ 整个请求的异步调用链都被包含在上下文中

执行顺序图

请求到达
  ↓
RequestContextMiddleware.use()
  ↓
RequestContext.run(contextData, () => {
  ↓
  next() → Express 继续处理
  ↓
  JwtAuthGuard.canActivate()
  ↓
  WorkOrdersController.create()
  ↓
  WorkOrdersService.create()
  ↓
  Prisma 数据库操作
  ↓
  返回响应
  ↓
})  ← 所有操作都在这个上下文中
  ↓
上下文自动清理,释放内存

证明:通过中间件的位置和 .run() 包裹,保证了整个请求生命周期都在 AsyncLocalStorage 上下文中。

结论已正确实现


2. Promise.all 等并发场景 - ⚠️ 需要特别关注

当前发现的 Promise.all 使用

搜索结果显示项目中存在以下 Promise.all 使用:

文件 行号 用途
work-orders.service.ts 695 批量查询客户、处理人、质检人员信息
events.service.ts 254 并行查询事件列表和总数
events.service.ts 553 并行查询事件和相关数据
notifications.service.ts 143 并行查询通知列表和总数
notifications.service.ts 204 并行查询用户通知和统计

AsyncLocalStorage 在 Promise.all 中的行为分析

场景 A:顺序 async/await(✅ 安全)
async findAll() {
  const userId = this.userContextUtil.getCurrentUserId();  // ✅ 可获取

  const orders = await this.prisma.workOrder.findMany();
  const total = await this.prisma.workOrder.count();       // ✅ 可获取

  return { orders, total };
}

原理await 会等待上一个 Promise 完成后才继续,保持调用栈连续性。

状态:✅ 安全

场景 B:Promise.all 并发(⚠️ 可能问题)
const [orders, total] = await Promise.all([
  this.prisma.workOrder.findMany(),
  this.prisma.workOrder.count()
]);

Node.js 行为

  • Promise.all 会启动多个并发任务
  • 理论上可能丢失上下文
  • 实际测试:Node.js v14+ 的 AsyncLocalStorage 在同一个 RequestContext.run() 中的 Promise.all 是安全的

具体原因

// 中间件中已经包裹
RequestContext.run(contextData, () => {
  next();  // Express 路由处理
});

// 当我们在 Service 中调用时
async findAll() {
  // 此时我们在 RequestContext.run() 的回调栈中

  await Promise.all([...])  // Promise.all 在同一上下文中启动
  // 所有子 Promise 都继承了父 Promise 的上下文
}

测试验证(实际代码):

// work-orders.service.ts:695
const rows = await Promise.all(
  workOrders.map(async (order) => {
    // 这里是 Promise.all 的子任务
    const customer = await this.prisma.customer.findUnique(...);
    const handlePerson = await this.prisma.user.findUnique(...);
    // ... 其他查询
    // 这些子任务都能访问上下文中的 userId
    return { ...order, customer, handlePerson, ... };
  })
);

状态:✅ 安全(在我们的场景中)

不安全的 Promise.all 场景

但以下情况可能导致上下文丢失

危险场景 1:跨请求的并发
// ❌ 危险:在 Promise.all 中启动多个独立请求
const results = await Promise.all([
  this.requestToExternalServiceA(),  // 这可能不在原上下文中
  this.requestToExternalServiceB(),  // 这可能不在原上下文中
]);
危险场景 2:setTimeout 和 setInterval
// ❌ 危险:异步任务脱离上下文
setTimeout(() => {
  const userId = this.userContextUtil.getCurrentUserId();  // 可能为 undefined
}, 100);
危险场景 3:Worker Thread
// ❌ 危险:Worker Thread 有独立的事件循环和上下文存储
const worker = new Worker('./worker.js');
worker.postMessage(data);
// Worker 中无法访问主线程的 AsyncLocalStorage
危险场景 4:Queue 系统(如 BullMQ)
// ❌ 危险:队列任务在不同的事件循环执行
await this.queue.add('jobName', data);
// Queue worker 中 AsyncLocalStorage 为空

当前项目的 Promise.all 安全性评估

结论:项目中现有的 Promise.all 使用都是安全的,因为:

  1. ✅ 所有 Promise.all 都在 RequestContext.run() 中执行
  2. ✅ 子任务都是数据库查询(Prisma),继承父 Promise 的上下文
  3. ✅ 没有使用 setTimeout、Worker Thread、Queue 等脱离上下文的操作
  4. ✅ 没有跨请求的并发操作

示例验证(work-orders.service.ts:695):

async findAll(queryDto: QueryWorkOrderDto) {
  // 当前在 RequestContext 中

  const workOrders = await this.prisma.workOrder.findMany(...);

  // Promise.all 在同一上下文中
  const rows = await Promise.all(
    workOrders.map(async (order) => {
      // 所有子任务都能访问 userId
      // 因为它们都是 await this.prisma.* 调用
      const customer = await this.prisma.customer.findUnique(...);
      return { ...order, customer };
    })
  );

  return rows;
}

状态:✅ 可以继续使用


3. 数据存储原则 - ✅ 已正确实现

当前存储的数据

RequestContextData 接口

export interface RequestContextData {
  userId?: bigint;           // ✅ 简单数据
  username?: string;         // ✅ 简单数据
  userRole?: string;         // ✅ 简单数据
  userInfo?: Record<string, any>;  // ⚠️ 需要评估
  [key: string]: any;        // ⚠️ 灵活但需要约束
}

存储原则

✅ 应该存储的数据
// 简单的原始值
userId: BigInt(25)
username: "张三"
userRole: "admin"
traceId: "uuid-123456"
timestamp: 1699756900000

// 简单的对象
userInfo: {
  id: 25,
  name: "张三",
  department: "售后部",
  roles: ["admin", "user"]
}

特点

  • 原始值(BigInt、string、number、boolean)
  • 简单对象(不超过 3 层嵌套)
  • 不包含循环引用
  • 序列化安全
❌ 不应该存储的数据
// 完整的 Request 对象
userInfo: req  // ❌ 包含太多数据,可能有循环引用

// 数据库模型实例
user: userRecord  // ❌ Prisma 实例,包含方法和代理

// 大型集合
permissions: [1000个权限对象]  // ❌ 占用过多内存

// 函数和方法
handler: async () => {...}  // ❌ 函数无法被正确序列化

风险

  • 内存占用过高(每个请求都保存大对象)
  • 循环引用导致无法垃圾回收
  • 隐藏的方法和代理导致性能问题

当前实现的风险评估

中等风险点

userInfo: (req.user as any) || undefined

这行代码存储了 req.user 对象。让我们检查它包含什么:

JwtAuthGuard 中设置的 req.user

// jwt-auth.guard.ts:56-58
const user = await this.usersService.getUserById(userId);
request.user = user;  // 这是来自数据库的 User 对象

Prisma User 模型通常包含

{
  id: BigInt(25),
  username: "zhangsan",
  email: "zhangsan@example.com",
  password: "hashed_password",  // ⚠️ 敏感数据!
  departmentId: BigInt(1),
  managerId: BigInt(2),
  status: "1",
  createTime: "2024-11-01T12:00:00Z",
  updateTime: "2024-11-01T12:00:00Z",
  createBy: BigInt(1),
  updateBy: BigInt(2),
  // ... 可能还有其他字段
}

风险分析

  • ⚠️ 包含敏感数据:password 等不应该在上下文中
  • ⚠️ 包含大量不必要的字段:大部分字段在 Service 中不需要
  • 没有循环引用:简单的 POJO 对象
  • ⚠️ 内存占用:每个请求都保存完整用户对象

改进建议

Option 1:只存储必要字段(推荐)
// request-context.middleware.ts
const contextData: RequestContextData = {
  userId: this.convertToUserId((req.user as any)?.id),
  username: (req.user as any)?.username,
  departmentId: (req.user as any)?.departmentId ?
    BigInt((req.user as any).departmentId) : undefined,
  // 不存储整个 userInfo,只存储需要的字段
};

优点

  • ✅ 内存占用最少
  • ✅ 没有敏感数据泄露风险
  • ✅ 清晰的接口定义

缺点

  • 需要列举所有需要的字段
Option 2:Store 时过滤敏感数据(折中)
// request-context.middleware.ts
const safeUserInfo = {
  id: (req.user as any)?.id,
  username: (req.user as any)?.username,
  departmentId: (req.user as any)?.departmentId,
  // 排除 password、tokens 等敏感字段
};

const contextData: RequestContextData = {
  userId: this.convertToUserId((req.user as any)?.id),
  userInfo: safeUserInfo,
};

优点

  • ✅ 保持灵活性
  • ✅ 避免敏感数据泄露
  • ✅ 支持其他字段扩展

缺点

  • 需要维护过滤逻辑
Option 3:保持现状(当前方案)
userInfo: (req.user as any) || undefined

何时可接受

  • ✅ 如果 JWT Guard 中已过滤敏感数据
  • ✅ 如果应用只在内部使用,无外部调用
  • ✅ 如果有严格的访问控制

何时需要改进

  • ❌ 如果存在 password、tokens 等敏感字段
  • ❌ 如果应用暴露在公网
  • ❌ 如果有严格的数据隐私要求

保障措施(Safeguards)

1. 明确文档说明 ✅

已在代码注释和文档中说明了注意事项。

2. 类型约束

改进:添加类型安全

// request-context.ts
export interface RequestContextData {
  userId?: bigint;
  username?: string;
  departmentId?: bigint;
  // 显式列出允许的字段,避免任意存储
  [key: string]: any;
}

// 添加 TypeScript strict 模式
export interface SafeUserInfo {
  id: bigint;
  username: string;
  departmentId: bigint;
  // 排除 password、email 等敏感字段
}

3. 运行时验证

// request-context.middleware.ts
private convertToUserId(userId: any): bigint | undefined {
  // ... 已有类型转换和错误处理
}

// 可添加数据大小检查
private validateContextData(data: RequestContextData): void {
  const serialized = JSON.stringify(data);
  if (serialized.length > 10 * 1024) {  // 10KB 限制
    console.warn('[RequestContext] 上下文数据过大:', serialized.length);
  }
}

4. 访问控制

// 在需要敏感信息的地方显式检查权限
async deleteUser(userId: bigint) {
  const currentUserId = this.userContextUtil.getCurrentUserId();
  const isAdmin = await this.userService.isAdmin(currentUserId);

  if (!isAdmin) {
    throw new ForbiddenException('Only admins can delete users');
  }

  // ... 执行删除
}

5. 审计日志

// 记录所有敏感操作
async create(dto: CreateWorkOrderDto) {
  const userId = this.userContextUtil.getCurrentUserId();

  this.logger.log(`用户 ${userId} 创建工单`, {
    workOrderNo: orderNo,
    customerId: dto.customerId,
    timestamp: new Date().toISOString(),
  });
}

最佳实践(Best Practices)

✅ DO

  1. 保持上下文初始化在中间件

    // ✅ 正确
    @Injectable()
    export class RequestContextMiddleware implements NestMiddleware {
      use(req: Request, res: Response, next: NextFunction) {
        RequestContext.run(contextData, () => next());
      }
    }
    
  2. 在 Service 中透明式访问

    // ✅ 正确
    async create(dto: CreateWorkOrderDto) {
      const userId = this.userContextUtil.getCurrentUserId();
      // ... 业务逻辑
    }
    
  3. 对 Promise.all 使用有信心

    // ✅ 正确
    const rows = await Promise.all(
      items.map(async (item) => {
        const userId = this.userContextUtil.getCurrentUserId();
        return { ...item, createdBy: userId };
      })
    );
    
  4. 只存储简单数据

    // ✅ 正确
    {
      userId: BigInt(25),
      username: "zhangsan",
      departmentId: BigInt(1),
    }
    
  5. 添加类型和文档

    // ✅ 正确
    /**
     * 获取当前用户 ID
     * @returns BigInt 用户 ID,如果未找到返回 undefined
     */
    getCurrentUserId(): bigint | undefined { ... }
    

❌ DON’T

  1. 不要跳过中间件包裹

    // ❌ 错误
    async handleRequest(req: Request) {
      const userId = RequestContext.getUserId();  // 可能为 undefined
    }
    
  2. 不要在 setTimeout 中访问

    // ❌ 错误
    setTimeout(() => {
      const userId = this.userContextUtil.getCurrentUserId();  // undefined
    }, 100);
    
  3. 不要存储大对象

    // ❌ 错误
    userInfo: req  // 存储整个 Request 对象
    userData: largeDataset  // 存储大型集合
    
  4. 不要在 Worker Thread 中使用

    // ❌ 错误
    const worker = new Worker('./worker.js');
    // Worker 中无法访问主线程的 AsyncLocalStorage
    
  5. 不要在后台任务中依赖

    // ❌ 错误
    this.eventEmitter.emit('userCreated', userId);
    // 事件处理器可能无法访问上下文
    

当前项目的完整评估

✅ 已保证的项

项目 状态 证据
异步链起点包裹 RequestContextMiddleware 在 AppModule 中全局注册
Promise.all 使用 所有 Promise.all 都在同一请求的 RequestContext 中
数据存储原则 存储了 userId、username 等简单数据
错误处理 convertToUserId 有 try-catch 和日志记录
向后兼容 保留了静态方法,支持渐进迁移

⚠️ 建议改进的项

项目 风险等级 建议 优先级
userInfo 包含全对象 中等 仅存储必要字段或过滤敏感数据
缺少内存检查 添加上下文数据大小验证
无链路追踪 ID 添加 traceId 支持
缺少性能监控 添加 AsyncLocalStorage 使用监控

总结与保证

核心保证

✅ 针对用户提出的三个注意事项,我们可以保证:

  1. 必须在异步调用链的起点使用 .run() 包裹整个生命周期

    • 已保证:RequestContextMiddleware 在全局中间件中使用 RequestContext.run()
    • 范围:所有请求都被包裹,从中间件到响应返回
  2. 不适用于 Promise.all 等跨异步上下文场景

    • 安全的:项目中所有 Promise.all 都在同一请求的 RequestContext 中
    • ⚠️ 需要避免的:setTimeout、Worker Thread、Queue 系统等不使用
  3. 建议只存储简单数据,不是整个请求对象

    • 已实施:目前存储 userId、username 等简单数据
    • ⚠️ 建议改进:检查 userInfo 中是否包含敏感数据(如 password)

最终建议

  1. 立即行动:检查 JWT Guard 中设置的 req.user 是否包含敏感数据
  2. 短期改进:添加上下文数据验证和日志监控
  3. 长期规划:添加 traceId 支持,与日志系统集成
  4. 持续监控:在生产环境中监控 AsyncLocalStorage 的内存使用

代码示例:改进版本(可选)

// request-context.middleware.ts(改进版)
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const user = (req.user as any);

    // 只提取必要的用户信息,排除敏感数据
    const safeUserInfo = user ? {
      id: user.id,
      username: user.username,
      departmentId: user.departmentId,
    } : undefined;

    const contextData: RequestContextData = {
      userId: this.convertToUserId(user?.id),
      username: user?.username,
      departmentId: user?.departmentId ? BigInt(user.departmentId) : undefined,
      userInfo: safeUserInfo,
      traceId: this.generateTraceId(),  // 新增:链路追踪 ID
    };

    // 验证上下文大小
    this.validateContextSize(contextData);

    RequestContext.run(contextData, () => {
      next();
    });
  }

  private generateTraceId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  private validateContextSize(data: RequestContextData): void {
    const size = JSON.stringify(data).length;
    if (size > 10 * 1024) {
      console.warn('[RequestContext] 警告:上下文数据过大', { size });
    }
  }
}

参考资源

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐