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/

相关推荐
_何同学1 小时前
Ollama 安装 DeepSeek 与 Spring Boot 集成指南
java·spring boot·后端·ai
饺子大魔王的男人2 小时前
【Three.js】机器人管线包模拟
javascript·机器人
知否技术2 小时前
知道这10个npm工具包,开发效率提高好几倍!第2个大家都用过!
前端·npm
希希不嘻嘻~傻希希2 小时前
CSS 字体与文本样式笔记
开发语言·前端·javascript·css·ecmascript
盖世英雄酱581363 小时前
时间设置的是23点59分59秒,数据库却存的是第二天00:00:00
java·数据库·后端
石小石Orz3 小时前
分享10个吊炸天的油猴脚本,2025最新!
前端
爱上妖精的尾巴4 小时前
3-19 WPS JS宏调用工作表函数(JS 宏与工作表函数双剑合壁)学习笔记
服务器·前端·javascript·wps·js宏·jsa
追逐时光者4 小时前
提高 .NET 编程效率的 Visual Studio 使用技巧和建议!
后端·.net·visual studio
草履虫建模4 小时前
Web开发全栈流程 - Spring boot +Vue 前后端分离
java·前端·vue.js·spring boot·阿里云·elementui·mybatis