nestjs 入门实战最强篇

nestjs 作为前端儿最简单的后端入门语言,在如此卷的今天,掌握一门后端语言是非常有必要的。

前言

通过这篇文章,你将学会:

  • 使用 typeorm 连接数据库,实现简单的 CRUD,实现接口的统一格式,自动生成 swagger 文档
  • 通过 docker + GITHUB ACTION 自动化部署到腾讯云服务器上,并通过域名访问接口,真正实战落地
  • 使用 JWT 实现用户注册登录,身份验证(token)拦截返回 401
  • 在服务器上安装 redis,并在 nest 中使用

文章大部分都是基于腾讯云服务器实现的,系统是 Linux CentOS如果你没有服务器也可以选择本地数据库,也可以新购一台,想学习后端知识服务器肯定是必备的。vscode 需要安装插件 Database Clientnode 版本选择高于 20 的,我的是 20.9.0

可以先行下载代码后阅读,代码地址,觉得还行的话希望能给仓库点个 star 。下面开始动手吧!

nest项目 初始化

  • 安装依赖并初始化 nest 项目
ruby 复制代码
# 全局安装 nest
$ npm i -g @nestjs/cli

# 创建 nest 项目
$ nest new project-name

# 安装依赖
$ yarn

贴一下我的项目结构

package.json 中修改 dev 的命令,便于调试,可以实时监听代码修改

json 复制代码
"dev": "nest start --watch",

nest 常用命令:

  • nest new 快速创建项目
  • nest generate 快速生成各种代码
  • nest build 使用 tsc 或者 webpack 构建代码
  • nest start 启动开发服务,支持 watch 和调试
  • nest info 打印 node、npm、nest 包的依赖版本
  • nest g resource xxx 快速创建 REST API 的模块

nest 的生命周期

  • 模块初始化:OnModuleInit
  • 应用启动:OnApplicationBootstrap
  • 模块销毁:OnModuleDestroy
  • 应用关闭前:BeforeApplicationShutdown
  • 应用正式关闭:OnApplicationShutdown

执行顺序如上顺序,在模块中 controller => service => module 文件依次执行

使用 typeorm 连接数据库

  • 在腾讯云服务器上新建一个数据库,并设置用户名跟密码,或者在本地安装数据库软件 Navicat 自行创建数据库
  • 前面我让大家安装的 Database Client,这时候可以打开,自行连接数据库,后面我们都在这里直接看数据库的数据变化
  • 安装数据库相关的依赖
sql 复制代码
yarn add @nestjs/typeorm typeorm mysql2 @nestjs/config -S
  • 在根目录下新建 .env .env.prod 配置文件
ini 复制代码
// 数据库地址
DB_HOST=
// 数据库端口
DB_PORT=3306
// 数据库登录名
DB_USER=
// 数据库登录密码
DB_PASSWD=
// 数据库名字
DB_DATABASE=
  • 根目录下新增配置文件 config/env.ts,后面需要给数据库连接使用
javascript 复制代码
import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV === 'production';

function parseEnv() {
  const localEnv = path.resolve('.env');
  const prodEnv = path.resolve('.env.prod');

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('缺少环境配置文件');
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
  return { path: filePath };
}
export default parseEnv();
  • src/app.module.ts 中添加数据库连接
typescript 复制代码
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import envConfig from '../config/env';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 设置为全局
      envFilePath: [envConfig.path],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql', // 数据库类型
        entities: [], // 数据表实体,synchronize为true时,自动创建表,生产环境建议关闭
        host: configService.get('DB_HOST'), // 主机,默认为localhost
        port: configService.get<number>('DB_PORT'), // 端口号
        username: configService.get('DB_USER'), // 用户名
        password: configService.get('DB_PASSWD'), // 密码
        database: configService.get('DB_DATABASE'), //数据库名
        timezone: '+08:00', //服务器上配置的时区
        synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
      }),
    }),
  ],
  controllers: [AppController],
  // 注册为全局守卫
  providers: [
    AppService,
  ],
})

执行 yarn dev 不报错则连接数据库成功

实现 CRUD

我们直接通过快捷命令生成 restful 风格的模块 posts,执行完会自动生成文件,并且会自动在 src/app.module.ts 中注册

nest g resource posts
  • src/posts/entities/posts.entity.ts ,熟悉 ts 的应该一看就懂了
less 复制代码
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('posts')
export class PostsEntity {
  @PrimaryGeneratedColumn() // 标记为主列,值自动生成
  id: number;

  @Column({ length: 50 })
  title: string;

  @Column({ length: 20 })
  author: string;

  @Column('text')
  content: string;

  @Column({ default: '' })
  thumb_url: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;
}
  • app.module.ts 中添加数据库表,添加后会根据 posts.entity.ts 定义的字段自动创建相应规则的表
javascript 复制代码
import { PostsEntity } from './posts/entities/posts.entity';

entities: [PostsEntity], // 在前面添加的数据库设置中添加

src/posts/posts.service.ts,下面的方法执行后就能在数据库看到相应的变化

typescript 复制代码
import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { getRepository, Repository } from 'typeorm';
import { PostsEntity } from './entities/posts.entity';

export interface PostsRo {
  list: PostsEntity[];
  count: number;
}
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository<PostsEntity>,
  ) {}

  // 创建文章
  async create(post: Partial<PostsEntity>): Promise<PostsEntity> {
    const { title } = post;
    if (!title) {
      throw new HttpException('缺少文章标题', 401);
    }
    const doc = await this.postsRepository.findOne({ where: { title } });
    if (doc) {
      throw new HttpException('文章已存在', 401);
    }
    return await this.postsRepository.save(post);
  }

  // 获取文章列表
  async findAll(query): Promise<PostsRo> {
    const qb = await getRepository(PostsEntity).createQueryBuilder('post');
    qb.where('1 = 1');
    qb.orderBy('post.create_time', 'DESC');

    const count = await qb.getCount();
    const { pageNum = 1, pageSize = 10, ...params } = query;
    qb.limit(pageSize);
    qb.offset(pageSize * (pageNum - 1));

    const posts = await qb.getMany();
    return { list: posts, count: count };
  }

  // 获取指定文章
  async findById(id): Promise<PostsEntity> {
    return await this.postsRepository.findOne({ where: { id } });
  }

  // 更新文章
  async updateById(id, post): Promise<PostsEntity> {
    const existPost = await this.postsRepository.findOne({ where: { id } });
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401);
    }
    const updatePost = this.postsRepository.merge(existPost, post);
    return this.postsRepository.save(updatePost);
  }

  // 刪除文章
  async remove(id) {
    const existPost = await this.postsRepository.findOne({ where: { id } });
    if (!existPost) {
      throw new HttpException(`id为${id}的文章不存在`, 401);
    }
    return await this.postsRepository.remove(existPost);
  }
}
  • 编写 create-post.dot.ts,定义字段类型
typescript 复制代码
export class CreatePostDto {
  readonly title: string;

  readonly author: string;

  readonly content: string;

  readonly cover_url: string;
}
  • 编写控制器文件,控制器也是接口地址的入口,接口传参,路由规则都是在这里,为了与其它模块区分,@Controller('post'),在最前面添加这个注解,之后的接口地址前面都需要带这个前缀。
less 复制代码
import { PostsService, PostsRo } from './posts.service';
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
  Query,
} from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dot';

@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  /**
   * 创建文章
   * @param post
   */
  @Post('/create')
  async create(@Body() post: CreatePostDto) {
    return await this.postsService.create(post);
  }

  /**
   * 获取所有文章
   */
  @Get('/findAll')
  async findAll(@Query() query): Promise<PostsRo> {
    return await this.postsService.findAll(query);
  }

  /**
   * 获取指定文章
   * @param id
   */
  @Get(':id')
  async findById(@Param('id') id) {
    return await this.postsService.findById(id);
  }

  /**
   * 更新文章
   * @param id
   * @param post
   */
  @Put(':id')
  async update(@Param('id') id, @Body() post) {
    return await this.postsService.updateById(id, post);
  }

  /**
   * 删除
   * @param id
   */
  @Delete('id')
  async remove(@Param('id') id) {
    return await this.postsService.remove(id);
  }
}
  • src/posts/posts.module.tsPostsEntity 关联数据库
python 复制代码
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsEntity } from './entities/posts.entity';

@Module({
  imports: [TypeOrmModule.forFeature([PostsEntity])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}
  • 此时的 src/app.module.ts 是这样的,有一些是在执行 nest g 时自动新增的,执行若有报错请检查以下是否漏了什么没写。
typescript 复制代码
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';
import envConfig from '../config/env';
import { PostsEntity } from './posts/entities/posts.entity';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 设置为全局
      envFilePath: [envConfig.path],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql', // 数据库类型
        entities: [PostsEntity], // 数据表实体,synchronize为true时,自动创建表,生产环境建议关闭
        host: configService.get('DB_HOST'), // 主机,默认为localhost
        port: configService.get<number>('DB_PORT'), // 端口号
        username: configService.get('DB_USER'), // 用户名
        password: configService.get('DB_PASSWD'), // 密码
        database: configService.get('DB_DATABASE'), //数据库名
        timezone: '+08:00', //服务器上配置的时区
        synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
      }),
    }),
    PostsModule,
  ],
  controllers: [AppController],
  providers: [
    AppService
  ],
})
export class AppModule {}

这时候我们执行 yarn dev,没有报错则前面步骤都很成功,然后我们打开本地软件 Apifox 或者 Postman,调用接口尝试

可以看到,此时已经将数据插入到数据库对应表了

接口返回统一格式

前面的接口返回格式需要做以下处理

封装全局错误的过滤器

  • 使用命令生成过滤器
css 复制代码
nest g filter core/filter/http-exception
  • 在自动生成的 src/core/filter/http-exceptionhttp-exception.filter.ts,写入
ini 复制代码
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse(); // 获取请求上下文中的 response对象
    const status = exception.getStatus(); // 获取异常状态码
    const exceptionResponse: any = exception.getResponse();
    let validMessage = '';

    for (let key in exception) {
      console.log(key, exception[key]);
    }
    if (typeof exceptionResponse === 'object') {
      validMessage =
        typeof exceptionResponse.message === 'string'
          ? exceptionResponse.message
          : exceptionResponse.message[0];
    }
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      message: validMessage || message,
      code: -1,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}

main.ts 中全局注册

javascript 复制代码
import { HttpExceptionFilter } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
   // 注册全局错误的过滤器
  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(9080);
}
bootstrap();

封装全局成功的拦截器

  • 使用命令生成拦截器
bash 复制代码
nest g interceptor core/interceptor/transform

拦截器代码实现:

python 复制代码
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          code: 0,
          msg: '请求成功',
        };
      }),
    );
  }
}

同样在 main.ts 中全局注册

javascript 复制代码
import { TransformInterceptor } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 全局注册拦截器
 app.useGlobalInterceptors(new TransformInterceptor())
  await app.listen(9080);
}
bootstrap();

至此接口返回的数据格式就统一了

自动生成 swagger 接口文档

  • 安装相关依赖
sql 复制代码
yarn add @nestjs/swagger swagger-ui-express -S
  • main.ts 中设置 Swagger
javascript 复制代码
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // 设置swagger文档
  const config = new DocumentBuilder()
    .setTitle('管理后台')
    .setDescription('管理后台接口文档')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(9080);
}
bootstrap();

这些描述需要我们分别在 controller 文件以及 dto 文件中添加注解

  • src/posts/posts.controller.ts ,使用 ApiOperation, ApiTags 添加注解
less 复制代码
import { ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('文章')
@Controller('post')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  /**
   * 创建文章
   * @param post
   */
  @ApiOperation({ summary: '创建文章' })
  @Post('/create')
  async create(@Body() post: CreatePostDto) {
    return await this.postsService.create(post);
  }
  ...
}
  • src/posts/dto/create-post.dot.ts,添加 ApiProperty, ApiPropertyOptional 注解
less 复制代码
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  @IsNotEmpty({ message: '文章标题必填' })
  readonly title: string;

  @IsNotEmpty({ message: '缺少作者信息' })
  @ApiProperty({ description: '作者' })
  readonly author: string;

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string;

  @ApiPropertyOptional({ description: '文章封面' })
  readonly cover_url: string;
}
  • 添加数据校验

安装相关依赖

csharp 复制代码
yarn add class-validator class-transformer -S

create-post.dto.ts 文件中添加验证, 完善错误信息提示

less 复制代码
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class CreatePostDto {
  @ApiProperty({ description: '文章标题' })
  @IsNotEmpty({ message: '文章标题必填' })
  readonly title: string;

  @IsNotEmpty({ message: '缺少作者信息' })
  @ApiProperty({ description: '作者' })
  readonly author: string;

  @ApiPropertyOptional({ description: '内容' })
  readonly content: string;

  @ApiPropertyOptional({ description: '文章封面' })
  readonly cover_url: string;
}

刷新文档地址就会生效了,少传作者信息也会报错了

json 复制代码
{
  "data": {},
  "message": "缺少作者信息",
  "code": -1
}

自动化部署 github action、docker

使用 GITHUB ACTION + docker ,在推送代码后自动构建 docker 镜像并推送代码到服务器,这一步跟我之前的 go 文章是大同小异的,可以参考 go 入门文章

  • 添加阿里云镜像
  • 创建命名空间后,创建该命名空间下的镜像,选择 github,绑定当前项目
  • 根目录下创建 Dockerfile

写入:

bash 复制代码
# 使用官方 Node 镜像
FROM node:20

# 创建并设置工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装依赖
RUN yarn install

# 复制源代码
COPY . .

# 构建 Nest 应用
RUN yarn build

# 绑定应用到 3000 端口
EXPOSE 3000

# 启动应用
CMD ["yarn", "start"]
  • Github 仓库中的新建 5 个 Repository secrets

    • SERVER_HOST:服务器 ip
    • SERVER_USERNAME:服务器登录用户名, 一般 root
    • SERVER_SSH_KEYGithub 的私钥,服务器需要要生成相对应的公钥,这里不会的可以参考我的 过去文章
    • ALIYUN_DOCKER_USERNAME:阿里云 Docker 用户名
    • ALIYUN_DOCKER_PASSWORD:阿里云 Docker 密码
  • 新增 .github/workflows/deploy.yml

yaml 复制代码
name: Deploy to Alibaba Cloud

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to Alibaba Cloud Container Registry
        run: |
          echo "${{ secrets.ALIYUN_DOCKER_PASSWORD }}" | docker login --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password-stdin registry.cn-shenzhen.aliyuncs.com

      - name: Build Docker image
        run: docker build -t registry.cn-shenzhen.aliyuncs.com/jiang-nest/jiang-nest-study:latest .

      - name: Push Docker image
        run: docker push registry.cn-shenzhen.aliyuncs.com/jiang-nest/jiang-nest-study:latest

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: SSH to server and deploy
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            docker login --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password ${{ secrets.ALIYUN_DOCKER_PASSWORD }} registry.cn-shenzhen.aliyuncs.com
            docker pull registry.cn-shenzhen.aliyuncs.com/jiang-nest/jiang-nest-study:latest
            docker ps -q --filter "name=jiang-nest-study" | grep -q . && docker stop jiang-nest-study || echo "Container jiang-nest-study is not running"
            docker ps -a -q --filter "name=jiang-nest-study" | grep -q . && docker rm jiang-nest-study || echo "Container jiang-nest-study does not exist"
            docker run -d --name jiang-nest-study -p 3000:3000 registry.cn-shenzhen.aliyuncs.com/jiang-nest/jiang-nest-study:latest
  • 腾讯云防火墙记得放开自定义端口 3000

推送代码后就自动部署了,然后打开 http://服务器ip:3000/docs

配置域名访问接口地址

前面我们使用的还是 ip 地址访问接口,但是在实际开发中是不可能只使用 ip 地址的,所以我们这里要设置可以通过域名访问接口。并且为了跟同域名下的其它功能区分,我们添加 /api 匹配。

nginx 配置文件 /www/server/nginx/conf/nginx.conf 中添加配置

bash 复制代码
server {
    listen 443 ssl;  # 启用 SSL 并监听 443 端口
    server_name junfeng530.xyz;  # 你的域名

    ssl_certificate /www/server/panel/vhost/cert/junfeng530.xyz/fullchain.pem;  # 替换为你的证书路径
    ssl_certificate_key /www/server/panel/vhost/cert/junfeng530.xyz/privkey.pem;  # 替换为你的私钥路径

    location /api/ {
        proxy_pass http://121.4.86.16:3000/;  # 代理到 Docker 容器所在的 3000 端口
        proxy_set_header Host $host;  # 保持 Host 头部
        proxy_set_header X-Real-IP $remote_addr;  # 获取真实 IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # 传递代理链 IP
        proxy_set_header X-Forwarded-Proto $scheme;  # 传递协议

        # 处理 URL 重写,将 /api 前缀移除
        rewrite ^/api/(.*)$ /$1 break;
    }
}

修改后重启 nginx,就可以通过域名 + /api 访问接口了,junfeng530.xyz/api/app/say...

添加静态资源访问

main.ts 中添加

javascript 复制代码
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(AppModule);

    // 支持静态资源
    app.useStaticAssets('public', { prefix: '/static' });
}

访问 junfeng530.xyz/api/static/... 生效,前面仍然需要加 /api 的前缀

使用 JWT 实现用户注册登录校验

JSON Web Token ,简称 JWT ,一种基于 JSON 的认证授权机制,是一个非常轻巧的标准规范。这个规范允许我们在用户和服务器之间传递安全可靠的信息。

  • 新增 auth 模块,前面已经做过一遍这样的操作了

    nest g resource auth

  • 定义字段:id name password,修改 src/auth/entities/auth.entity.ts

kotlin 复制代码
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('auth')
export class AuthEntity {
  // id为主键并且自动递增
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;
}
  • auth.module.ts 中将数据库注入
python 复制代码
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { AuthEntity } from './entities/auth.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([AuthEntity])],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}
  • app.module.ts 中自动注册该表,执行后会自动创建 auth
javascript 复制代码
import { AuthModule } from './auth/auth.module';
import { AuthEntity } from './auth/entities/auth.entity';

entities: [AuthEntity],
  • auth.service.ts 中编写注册登录方法
typescript 复制代码
import { Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthEntity } from './entities/auth.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(AuthEntity)
    private readonly authRepository: Repository<AuthEntity>,
  ) {}

  // 注册
  signup(signupData: CreateAuthDto) {
    console.log(signupData, this.authRepository);
    return '注册成功';
  }

  // 登录
  login(loginData: CreateAuthDto) {
    console.log(loginData);
    return '登录成功';
  }
}
  • 执行 yarn dev,数据已经创建好 auth 的表
  • auth.controller.ts,编写登录注册方法
typescript 复制代码
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateAuthDto } from './dto/create-auth.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  /**
   * 注册
   * @param name 姓名
   * @param password 密码
   */
  @Post('/signup')
  signup(@Body() signupData: CreateAuthDto) {
    return this.authService.signup(signupData);
  }

  /**
   * 登录
   * @param name 姓名
   * @param password 密码
   */
  @Post('/login')
  login(@Body() loginData: CreateAuthDto) {
    return this.authService.login(loginData);
  }
}
  • 编写 create-auth.dto.ts,定义字段规则
less 复制代码
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class CreateAuthDto {
  @ApiProperty({ description: '姓名' })
  @IsNotEmpty({ message: '姓名必填' })
  readonly username: string;

  @ApiProperty({ description: '密码' })
  @IsNotEmpty({ message: '密码必填' })
  readonly password: string;
}

apiFox 测试接口 http://127.0.0.1:3000/auth/signup

数据库已插入,接下来写 jwt 的逻辑

安装相关依赖

sql 复制代码
yarn add bcryptjs @nestjs/jwt
  • 新建 token 配置文件 src/common/constants.ts
arduino 复制代码
export const jwtConstants = {
    secret: "leeKey", // 密钥
    expiresIn: "60s" // token有效时间
}
  • auth.module.ts 中配置 jwt
css 复制代码
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from '../common/constants';

imports: [
    TypeOrmModule.forFeature([AuthEntity]),
    JwtModule.register({
    secret: jwtConstants.secret,
    signOptions: { expiresIn: jwtConstants.expiresIn },
    }),
],
  • 编写注册登录逻辑,src/auth/auth.service.ts
typescript 复制代码
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import { AuthEntity } from './entities/auth.entity';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcryptjs from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
import { RedisService } from '../redis/redis.service';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(AuthEntity) private readonly auth: Repository<AuthEntity>,
    private readonly JwtService: JwtService,
    private readonly redisService: RedisService, // 注册redis控制器
  ) {}

  // 注册
  async signup(signupData: CreateAuthDto) {
    const findUser = await this.auth.findOne({
      where: { username: signupData.username },
    });
    if (findUser && findUser.username === signupData.username)
      return '用户已存在';
    // 对密码进行加密处理
    signupData.password = bcryptjs.hashSync(signupData.password, 10);
    await this.auth.save(signupData);
    // 尝试将注册成功的用户存入redis中
    this.redisService.set(signupData.username, signupData.password);
    return '注册成功';
  }

  // 登录
  async login(loginData: CreateAuthDto) {
    const findUser = await this.auth.findOne({
      where: { username: loginData.username },
    });
    // 没有找到
    if (!findUser) return new BadRequestException('用户不存在');

    // 找到了对比密码
    const compareRes: boolean = bcryptjs.compareSync(
      loginData.password,
      findUser.password,
    );
    // 密码不正确
    if (!compareRes) return new BadRequestException('密码不正确');
    const payload = { username: findUser.username };

    return {
      access_token: this.JwtService.sign(payload),
      msg: '登录成功',
    };
  }
}

身份验证拦截

  • 安装依赖
sql 复制代码
yarn add @nestjs/passport passport-jwt passport
yarn add -D @types/passport-jwt
  • 编写自定义装饰器,新增 src/common/public.decorator.ts
javascript 复制代码
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = 'isPublic'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
  • 新建 src/auth/jwt-auth.grard.ts 文件,用于全局守卫,将未携带 token 的接口进行拦截
typescript 复制代码
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../common/public.decorator';

@Injectable()
export class jwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    console.log(isPublic, 'isPublic');
    if (isPublic) return true;
    return super.canActivate(context);
  }
}
  • 新建 验证策略文件 /src/auth/jwt-auth.strategy.ts
typescript 复制代码
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtConstants } from '../common/constants';

export interface JwtPayload {
  username: string;
}

@Injectable()
// 验证请求头中的token
export default class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: JwtPayload) {
    console.log(payload.username);
    const { username } = payload;
    return {
      username,
    };
  }
}
  • auth.module.tsproviders 中配置 JwtAuthStrategy
python 复制代码
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthEntity } from './entities/auth.entity';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from '../common/constants';
import JwtAuthStrategy from './jwt-auth.strategy';

@Module({
imports: [
    TypeOrmModule.forFeature([AuthEntity]),
    JwtModule.register({
        secret: jwtConstants.secret,
        signOptions: { expiresIn: jwtConstants.expiresIn },
       }),
    ],
    controllers: [AuthController],
    providers: [AuthService, JwtAuthStrategy],
})
export class AuthModule {}
  • app.module.ts 将其注册为全局守卫
javascript 复制代码
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.grard';

// 注册全局守卫
providers: [
AppService,
    {
        provide: APP_GUARD,
        useClass: jwtAuthGuard,
    },
],

此时,请求注册接口 http://127.0.0.1:3000/auth/signup, 将会返回 401

  • 给通用接口(注册和登录接口)都加上 @Public 装饰器,绕过检测 src/auth/auth.controller.ts
kotlin 复制代码
import { Public } from 'src/common/public.decorator';

 /**
   * 注册
   * @param name 姓名
   * @param password 密码
   */
  @Public() 在这里添加 @Public
  @Post('/signup')
  signup(@Body() signupData: CreateAuthDto) {
    return this.authService.signup(signupData);
  }

至此,再次请求注册接口 http://127.0.0.1:3000/auth/signup, 就可以直接绕过 token 校验了

接入 redis

在腾讯云服务器上安装 redis

在腾讯云终端执行,不同的操作系统命令不同,这里是 Linux CentOS

shell 复制代码
# 安装 redis
$ yum install redis

# 启动 redis
$ systemctl start redis

# 设置开机自启
$ systemctl enable redis

# 验证 redis 是否正常启动
$ redis-cli ping
  • 安装完后,编辑 /etc/redis.conf,设置 redis 配置,连接到服务器,bind 值需要改成:bind 0.0.0.0
bash 复制代码
设置密码:
requirepass jiang

Redis 监听地址和端口:
bind 127.0.0.1 // 需要连接到服务器,修改成自己的 0.0.0.0

修改端口
port 6379 # 默认为 6379

修改配置后重启 redis

systemctl restart redis

在 nest 中使用 redis

ioredis 是国内 nodejs 的最广泛的 redis 客户端

  • 安装依赖
csharp 复制代码
yarn add ioredis
  • 创建 redis 管理模块,这里只需创建 service、module 文件
arduino 复制代码
nest generate service redis
nest generate module redis
  • 设置 redis 连接,并注册一些方法,在生成的 redis.service.ts 中写入
typescript 复制代码
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisService {
  private redisClient: Redis;

  constructor() {
    // 配置 Redis 连接
    this.redisClient = new Redis({
      host: '121.4.86.16', // Redis 服务器地址
      port: 6379, // Redis 端口
      password: 'jiang', // 如果设置了密码,请输入
      db: 0, // 使用的 Redis 数据库,默认为 0
    });
  }

  // 通过 get 方法访问 Redis 中的键值
  async get(key: string): Promise<string> {
    return await this.redisClient.get(key);
  }

  // 通过 set 方法将值存入 Redis 中
  async set(key: string, value: string): Promise<void> {
    await this.redisClient.set(key, value);
  }

  // 关闭 Redis 客户端连接
  async onModuleDestroy() {
    await this.redisClient.quit();
  }
}
  • 编写 redis.module.ts
python 复制代码
import { Module } from '@nestjs/common';
import { RedisService } from './redis.service';

@Module({
  providers: [RedisService],
  exports: [RedisService],
})
export class RedisModule {}
  • 腾讯云服务器需要放开 6379 端口

在其它模块中使用 redis

  • 步骤一:在 src/auth/auth.module.ts 中导入 redis.module.ts
php 复制代码
import { RedisModule } from '../redis/redis.module'; // 添加

imports: [
    TypeOrmModule.forFeature([AuthEntity]),
    JwtModule.register({
        secret: jwtConstants.secret,
        signOptions: { expiresIn: jwtConstants.expiresIn },
    }),
    RedisModule, // 添加
],
  • 步骤二:在 auth.service.ts 中导入 redis.service.ts,并调用方法使用
typescript 复制代码
import { RedisService } from '../redis/redis.service';

constructor(
private readonly redisService: RedisService, // 注册redis控制器
) {}

// 尝试将注册成功的用户存入redis中
this.redisService.set(signupData.username, signupData.password);
  • apiFox 中调用 http://127.0.0.1:3000/auth/signup

  • 在插件 Database 中,连接 redis 数据库,用户名不用填写,其它正常填写,连接成功后,会发现多了一条刚刚注册的用户信息

至此,你也入门 nestjs 了,真棒,能够看到这里相信你一定有所收获,觉得不错可以给文章点个赞~

相关推荐
Oak Zhang31 分钟前
sharding-jdbc自定义分片算法,表对应关系存储在mysql中,缓存到redis或者本地
redis·mysql·缓存
门牙咬脆骨1 小时前
【Redis】redis缓存击穿,缓存雪崩,缓存穿透
数据库·redis·缓存
门牙咬脆骨1 小时前
【Redis】GEO数据结构
数据库·redis·缓存
周三有雨3 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
墨鸦_Cormorant3 小时前
使用docker快速部署Nginx、Redis、MySQL、Tomcat以及制作镜像
redis·nginx·docker
Dlwyz6 小时前
问题: redis-高并发场景下如何保证缓存数据与数据库的最终一致性
数据库·redis·缓存
飞升不如收破烂~7 小时前
redis的List底层数据结构 分别什么时候使用双向链表(Doubly Linked List)和压缩列表(ZipList)
redis
吴半杯8 小时前
Redis-monitor安装与配置
数据库·redis·缓存
小王码农记9 小时前
vue中路由缓存
前端·vue.js·缓存·typescript·anti-design-vue
会code的厨子10 小时前
Redis缓存高可用集群
redis·缓存