Nest.js 最佳实践:异步上下文(Context)实现自动填充

Nest.js 最佳实践:异步上下文(Context)实现自动填充

在开发企业级 Web 应用的时候,经常会在设计数据表的时候设计一些 createBy 或者 updateBy 这种类似的字段,有使用 Java 开发过 Web 的朋友都应该知道能够使用 ThreadLocal 这种类似的方案能够在一个完整的请求当中存储上下文信息,确保同一 HTTP 请求可以随时"拿到"当前登录用户。

本篇文章是基于 Node.js 14 以后提供的 AsyncLocalStorage 以及 nestjs-cls 开源库来实现一个请求过程当中自动填充数据的教程。

一、初始化 Nest.js 项目

创建 Nest.js 项目

bash 复制代码
npm i -g @nestjs/cli
nest new nest-context-demo
cd nest-context-demo

这里我直接使用自己搭建的一个脚手架项目,Github 地址是: github.com/DimplesY/ne...

当然也希望能够给个 star 支持一下 🎉。

使用如下命令即可

bash 复制代码
npx degit DimplesY/nest-api-starter nest-context-demo

创建数据库容器

进入到项目目录,使用下面的 docker compose 命令启动一个 postgres

bash 复制代码
cd nest-context-demo
docker compose up postgres -d

并且在项目的根目录下创建一个 .env 文件,贴入下面的内容

shell 复制代码
DATABASE_URL=postgres://postgres:123456@localhost:5432/api

REDIS_PASSWORD=123456

随后使用 pnpm install 安装项目的依赖。

使用 pnpm run db:push 命令将我们项目内的数据表同步到数据库当中,如下图所示。

然后执行pnpm run db:studio ,用浏览器打开 local.drizzle.studio/ ,看到如下界面即表示我们的数据表都成功的创建到了数据库当中。

因为本教程主要教的内容是实现一个请求内共享的上下文,就不再对 drizzle-orm 进行过多的介绍,如果你想学习,可以阅读官方文档: orm.drizzle.team/docs/overvi...

创建一个 test 用户,并且密码为 123456,如下所示。

密码生成工具在 @/common/utils/password.util.ts,你可以调用来生成其他密码。

在还没介绍 nest-cls 之前,我们先对 app.module.ts 进行一下修改,代码如下,直接将 nest-cls 相关的代码注释掉,代码如下。

ts 复制代码
// src/app.module.ts

import { Module } from '@nestjs/common';
import { LoggerModule } from '@/logger/logger.module';
import { ConfigModule } from '@nestjs/config';
import { DrizzleModule } from './drizzle/drizzle.module';
// import { ClsModule } from 'nestjs-cls';
import { AuthModule } from './modules/auth/auth.module';

@Module({
  imports: [
    // ClsModule.forRoot({
    //   middleware: {
    //     mount: true,
    //   },
    //   global: true,
    // }),
    DrizzleModule,
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    LoggerModule,
    AuthModule,
  ],
})
export class AppModule {}
ts 复制代码
// /src/common/guards/auth.guard.ts

import { AuthService } from '@/modules/auth/auth.service';

import {
  CanActivate,
  ExecutionContext,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
// import { ClsService } from 'nestjs-cls';
import { BizException } from '@/common/exceptions/biz.exception';
import { DrizzleService } from '@/drizzle/database.provider';
import {
  ExpressBizRequest,
  getNestExecutionContextRequest,
} from '../transformers/get-req.transformer';
import { UserSelectType } from '@/drizzle/schema/users';

function isJWT(token: string): boolean {
  const parts = token.split('.');
  return (
    parts.length === 3 &&
    /^[a-zA-Z0-9_-]+$/.test(parts[0]) &&
    /^[a-zA-Z0-9_-]+$/.test(parts[1]) &&
    /^[a-zA-Z0-9_-]+$/.test(parts[2])
  );
}

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly drizzle: DrizzleService,
    // private readonly cls: ClsService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<any> {
    const request = this.getRequest(context);

    const query = request.query as Record<string, any>;
    const headers = request.headers;
    const Authorization: string =
      headers.authorization ||
      (headers.Authorization as string) ||
      (query.token as string);

    if (!Authorization) {
      throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
    }

    const jwt = Authorization.replace(/[Bb]earer /, '');

    if (!isJWT(jwt)) {
      throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
    }

    const payload = await this.authService.verifyToken(jwt);
    if (!payload.id) {
      throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
    }

    const user = await this.drizzle.db.query.users.findFirst({
      where: (user, { eq }) => eq(user.id, payload.id),
    });

    if (!user) {
      throw new BizException(HttpStatus.UNAUTHORIZED, '请登录');
    }

    this.attachUserAndToken(request, user, Authorization);
    // this.cls.set('user', payload);
    return true;
  }

  private getRequest(context: ExecutionContext) {
    return getNestExecutionContextRequest(context);
  }

  private attachUserAndToken(
    request: ExpressBizRequest,
    user: UserSelectType,
    token?: string,
  ) {
    request.user = user;
    request.token = token;
  }
}

接下来我们需要自己动手实现一个存储上下文信息的工具,创建 context/user-context.ts 文件,代码如下:

ts 复制代码
// context/user-context.ts
import { AsyncLocalStorage } from 'async_hooks';

export interface UserContext {
  id?: number;
}

export const userAsyncStorage = new AsyncLocalStorage<UserContext>();

创建一个拦截器 common/interceptors/user-context.interceptor.ts

ts 复制代码
import { userAsyncStorage } from '@/context/user-context';
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { getNestExecutionContextRequest } from '../transformers/get-req.transformer';

@Injectable()
export class UserContextInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = getNestExecutionContextRequest(context);
    const user = request.user;
    return userAsyncStorage.run(user ? { id: user.id } : {}, () =>
      next.handle(),
    );
  }
}

在全局注册上面定义的 user-context.interceptor.tsmain.ts 代码如下:

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@/app.module';
import { APP_PORT } from '@/app.config';
import { WinstonModule } from 'nest-winston';
import { logger } from '@/logger/logger.global';
import { UserContextInterceptor } from './common/interceptors/user-context.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger({ instance: logger }),
  });

  app.useGlobalInterceptors(new UserContextInterceptor());
  await app.listen(APP_PORT);
}

void bootstrap();

接下来我们再来创建一些工具函数,方便我们在上下文当中获取信息。

ts 复制代码
// context/current-user.ts
import { userAsyncStorage } from './user-context';

export function getCurrentUser() {
  return userAsyncStorage.getStore();
}

OK,到这里我们已经实现了一个自己的存储异步上下文的工具,能够在一个请求当中随时获取当前用户的信息。

效果展示

创建一个接口来测试一下,修改 auth.controller.ts ,添加下面的代码。

ts 复制代码
  @Auth()
  @Get('/profile')
  handleProfile() {
    const user = getCurrentUser();
    return user;
  }

先登录一下,获得一个用于用户信息认证的 token,如下图所示(我是用的 apifox)。

在测试一下我们刚刚新增个接口,如下图所示,可以看到我们已经成功的拿到了用户的 id 信息。

至此,我们已经成功的实现了一个类似 Java 里面 ThreadLocal 的功能,能够在一个请求的处理函数的整个过程当中直接获取用户的 id,当然你也可以存储其他信息。

在正式项目当中还是直接使用项目已经集成的 nest-cls,原理在本篇教程当中实现的都大差不差。

nest-cls 官方文档地址:papooch.github.io/nestjs-cls/

相关推荐
Nan_Shu_61418 小时前
学习: Threejs (2)
前端·javascript·学习
G_G#18 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界18 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路19 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
勇哥java实战分享19 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
是一个Bug19 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213819 小时前
React面向组件编程
开发语言·前端·javascript
学历真的很重要19 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn089519 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪19 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端