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/

相关推荐
LuciferHuang2 小时前
震惊!三万star开源项目竟有致命Bug?
前端·javascript·debug
GISer_Jing2 小时前
前端实习总结——案例与大纲
前端·javascript
天天进步20152 小时前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
姑苏洛言3 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手3 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
姑苏洛言3 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友3 小时前
🍃认识一下boomi
后端
苏三说技术3 小时前
MySQL的三大日志
后端
豌豆花下猫4 小时前
让 Python 代码飙升330倍:从入门到精通的四种性能优化实践
后端·python·ai
hackchen4 小时前
Go与JS无缝协作:Goja引擎实战之错误处理最佳实践
开发语言·javascript·golang