安装nestjs

npm i -g @nestjs/cli

创建项目

nest new project-name

可以选择npm、yarn、npmp,这里选择yarn

nestjs能否代替Java nestjs项目_node.js


再依赖安装完毕之后,可以使用如下命令启动 NestJS 应用,然后浏览器即可访问 http://localhost:3000/ :出现如下界面即代表项目已经正常启动了。

数据库

安装依赖

选用mysql数据库,安装数据库依赖。

yarn add mysql typeorm @nestjs/typeorm

配置数据库

配置数据库,在app.module.ts

// 根模块用于处理其他类的引用与共享。
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { MenuModule } from './menu/menu.module';
import { RoleModule } from './role/role.module';
import { UserRoleModule } from './user_role/user_role.module';
import { join } from 'path';

@Module({
  imports: [
    // 加载连接数据库
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '123456',
      database: 'vite_node',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
      logging: false,
    }),
    // 子模块
    UserModule,
    AuthModule,
    MenuModule,
    RoleModule,
    UserRoleModule,
  ],
  controllers: [AppController],
  providers: [AppService],
  
})
export class AppModule {}

数据表

以下是基础数据表

nestjs能否代替Java nestjs项目_前端_02


menu.sqlrole.sqlrole_menu.sqluser.sqluser_role.sql

使用swagger

安装依赖

yarn add @nestjs/swagger swagger-ui-express

配置swagger

nest-cli.jsonmain.ts文件中进行配置,然后启动项目访问http:localhost:3001/docs 即可。

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "generateOptions": {
    "spec": false
  },
  "compilerOptions": {
    "plugins": ["@nestjs/swagger"]
  }
}
// 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
// Swagger 模块
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';

//配置 swagger
const setupSwagger = (app) => {
  const config = new DocumentBuilder()
    .addBearerAuth()
    .setTitle('NEST API')
    .setDescription('nest-test的 API 文档')
    .setVersion('1.0')
    .build();
  // 创建
  const document = SwaggerModule.createDocument(app, config);
  // 启动
  SwaggerModule.setup('doc', app, document, {
    swaggerOptions: {
      persistAuthorization: true,
    },
  });
};
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  // 传入app 访问http:localhost:3001/doc
  setupSwagger(app);
  await app.listen(3001);
}
bootstrap();

根据数据库数据表生成entity-typeorm-model-generator

"db": "npx typeorm-model-generator -h localhost -d vite_node -p 3306 -u root -x 123456 -e mysql -o ./src/entities --noConfig true --ce pascal --cp camel -a"
  • npx typeorm-model-generator :如果全局安装了,npx就不用
  • -h localhost :ip
  • -d vite_node :数据库名字
  • -p 3306 :数据库端口号
  • -u root :用户名
  • -x 123456 :密码
  • -e mysql :什么数据库
  • -o ./src/entities :生成实体的地址
  • –noConfig true :表示不生成ormconfig.jsontsconfig.json文件
  • –ce pascal :类名转换成首字母大写的驼峰命名
  • –cp camel :将数据库中的字段也转换成驼峰命名,比如user_role转换成userRole
  • -a:表示实体会继承BaseEntity的类

类如User,关于一些注解的说明。

  • @PrimaryGeneratedColumn:主键id
  • @ApiProperty:关于这个字段的说明
  • @Column
  • nullable:是否可以为空。
  • name:此字段表示在数据库中的列名,类如驼峰命名转换:login_name ——> loginaName。
  • select:查询时是否返回此字段
  • @Exclude:使用此注解时,可以实现对返回的数据过滤掉此列,需要在controller层对某一个方法上加上注解 @UseInterceptors(ClassSerializerInterceptor)对应@Exclude()列,表示此列隐藏。
  • @OneToMany:一对多的关系
import {
  BaseEntity,
  Column,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";
import { ApiProperty } from '@nestjs/swagger';
import { UserRole } from "./UserRole.entity";
import { Exclude } from "class-transformer";
import { IsEnum } from "class-validator";

@Entity("user", { schema: "vite_node" })
export class User extends BaseEntity {
  @ApiProperty({ description: '自增 id' })
  @PrimaryGeneratedColumn({ type: "int", name: "id" })
  id: number;

  // 表示select: false 查询user时不会返回这个字段,隐藏此列,也可以使用
 // @Column({ select: false })
  @ApiProperty({ description: '密码' })
  @Exclude()
  @Column("varchar", { name: "password", length: 255 })
  password: string;

  @ApiProperty({ description: '性别',  example: 1,  required: false,  enum: [0, 1], }) 
  @IsEnum(
    { 女: 0, 男: 1 },
    {
      message: ' sex 只能传入数字0或1',
    },
  )
  @Column("int", { default: 1, name: "sex",})
  sex?: number;

  @ApiProperty({ description: '邮箱',  required: false })
  @Column("varchar", { name: "email", nullable: true, length: 255 })
  email: string | null;

  @ApiProperty({ description: '地址' })
  @Column("varchar", { name: "address", nullable: true, length: 255 })
  address: string | null;

  @ApiProperty({ description: '登录名' })
  @Column("varchar", { name: "login_name", length: 255 })
  loginName: string;

  @ApiProperty({ description: '用户名' })
  @Column("varchar", { name: "user_name", length: 255 })
  userName: string;

  @OneToMany(() => UserRole, (userRole) => userRole.user)
  userRoles: UserRole[];
}

创建资源 CRUD 生成器

这里先创建user表的

nest g resource [name]
// 简写 res
nest g res user

nestjs能否代替Java nestjs项目_前端_03

]

登录-权限-认证-守卫 jwt

安装依赖

  • @nestjs/passport:模块将该框架包装在一个 Nest 风格的包中,使其易于集成到 Nest 应用程序中。
  • @nestjs/jwt :身份认证
  • @types/passport-jwt:编写 TypeScript 代码时提供了帮助
  • passport-jwt:策略包(还有passport-local)
  • passport:是node目前最流行的身份认证库,与使用@nestjs/passport的nestjs结合使用非常简单。

策略有本地策略和jwt策略,这里使用的是jwt

yarn add @nestjs/jwt passport-jwt @types/passport-jwt @nestjs/passport passport

创建auth相关模块(module、service、controller)

nest g mo auth
nest g s auth
nest g c auth

nestjs能否代替Java nestjs项目_mysql_04


AuthService中需要进行登录验证的时候需要操作UserService,所以在UserModule的exports中将其暴露出去,以供其他模块使用。然后在AuthModule的providers中添加UserService

import { AuthModule } from '../auth/auth.module';
import { UserRole } from '../entities/UserRole.entity';
import {
  Module,
  NestModule,
  MiddlewareConsumer,
  RequestMethod,
  forwardRef,
} from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../entities/User.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { LoggerMiddleware } from '../middle-ware/logger.middleware';

@Module({
  imports: [TypeOrmModule.forFeature([User, UserRole])],
  controllers: [UserController],
  providers: [UserService],
  // 将 UserService 暴露出去
  exports: [UserService]
})
export class UserModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'user', method: RequestMethod.GET });
  }
}
import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";

@Module({
  imports: [
    JwtModule.register({
      secret: JWT_CONSTANT.secret,
      signOptions: { expiresIn: '4h' }
    }),
    // 使用 UserModule 的内容
    UserModule
  ],
  providers: [
    AuthService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, //挂载全局接口
    },
  ],
  controllers: [AuthController],
})
export class AuthModule {}

然后需要创建jwt.strategy.ts写jwt的验证策略。

import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { JWT_CONSTANT } from "./jwt.constant";
import { User } from "src/entities/User.entity";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: JWT_CONSTANT.secret,
    });
  }

  async validate(payload: User) {
    return { loginName: payload.loginName, id: payload.id };
  }
}

然后在写一个守卫来做一个验证。

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
  SetMetadata,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { AuthGuard } from "@nestjs/passport";
import { RedisInstance } from "src/utils/redis";
// @SkipAuth 跳过JWT验证
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private reflector: Reflector,
    private jwtService: JwtService) {
    super();
  }
  async canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    console.log('---re--',request);
    const authorization = request["headers"].authorization || void 0;
    let tokenNotTimeOut = true;
    if (authorization) {
      const token = authorization.split(" ")[1]; // authorization: Bearer xxx
      try {
        let payload: any = this.jwtService.decode(token);
        const key = `${payload.id}-${payload.loginName}`;
        const redis_token = await RedisInstance.getRedis(
          "jwt-auth.guard.canActivate",
          0,
          key
        );
        if (!redis_token || redis_token !== token) {
          throw new UnauthorizedException("请重新登录");
        }
      } catch (err) {
        tokenNotTimeOut = false;
        throw new UnauthorizedException("请重新登录");
      }
    }
    
    return  tokenNotTimeOut && (super.canActivate(context) as boolean);
    // return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    // You can throw an exception based on either "info" or "err" arguments
    console.log(err, user, info);
    
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}
// swager装饰器:需要验证token的controller中增加装饰器@ApiBearerAuth,增加后swager请求header会携带Authorization参数。
// 生成跳过检测装饰器 @SkipAuth()
export const IS_PUBLIC_KEY = "isPublic";
export const SkipAuth = () => SetMetadata(IS_PUBLIC_KEY, true);

然后我们将其引入到auth模块中,再写登录接口

import { UserModule } from './../user/user.module';
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";

@Module({
  imports: [
    JwtModule.register({
      secret: JWT_CONSTANT.secret,
      signOptions: { expiresIn: '4h' }
    }),
    // 使用 UserModule 的内容
    UserModule
  ],
  providers: [
    AuthService,
    JwtStrategy,
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, //挂载全局接口
    },
  ],
  controllers: [AuthController],
})
export class AuthModule {}
import { Body, Controller, Post } from "@nestjs/common";
import { ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger";
import { LoginUserDto } from "./dto/login-user.dto";
import { AuthService } from "./auth.service";
import { SkipAuth } from "./jwt-auth.guard";// 跳过登录

@Controller("auth")
@ApiTags("用户验证")
export class AuthController {
  constructor(private authService: AuthService) {}
  
  @SkipAuth()
  @Post("login.do")
  @ApiBody({type:LoginUserDto})
  @ApiOperation({
    summary: "用户登录",
  })
  async loginUser(@Body() loginUserDto: LoginUserDto) {
    return await this.authService.login(loginUserDto);
  }
}

这里需要注意的是,登录时密码加密使用的是crypto,token存储使用redis来进行对比,下面部分会单独说明redis的引入。
AuthService中使用UserService中方法findOneByLoginNamegetUserRole。( private readonly userService: UserService)

import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { LoginUserDto } from "./dto/login-user.dto";
import { Encrypt } from 'src/utils/crypto'
import { UserService } from "../user/user.service";
import { RedisInstance } from "src/utils/redis";

const logger = new Logger("auth.service");

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private jwtService: JwtService
  ) { }

  /**
   * @description: 用户登录
   * @param {User} loginUserDto
   * @return {*}
   */
  public async login(loginUserDto: LoginUserDto) {
    let data = {}
    try {
      const loginName: string = loginUserDto.loginName;
      const password: string = loginUserDto.password;

      const userInfo = await this.userService.findOneByLoginName(loginName);
      if (userInfo.length === 0) {
        data = { flag: false, msg: "用户不存在" };
        return;
      }
      // 加密
      // const pass = Encrypt(password)

      // if (pass === userInfo[0].password) {
      if (password === userInfo[0].password) {
        const token = await this.createToken(userInfo[0]);

        //存储token到redis
        const redis = await RedisInstance.initRedis("auth.login", 0);
        const key = `${userInfo[0].id}-${loginUserDto.loginName}`;
        await RedisInstance.setRedis("auth.login", 0, key, `${token}`);
        data = {
          flag: true,
          msg: "登录成功",
          user: {
            userInfo: userInfo[0],
          },
          token,
        };
      } else {
        data = { flag: false, msg: "用户密码错误" };
      }
    } catch (error) {
      logger.log(error);
      data = { flag: false, msg: "登录失败" };
    } finally {
      return data;
    }
  }

  /**
   * @description:创建token
   * @param loginUserDto 
   * @returns 
   */
  private async createToken(loginUserDto: LoginUserDto) {
    const payload = {
      loginName: loginUserDto.loginName,
      password: loginUserDto.password,
      id: loginUserDto.id
    };
    return this.jwtService.sign(payload);
  }
}

UserService 中实现以上所调用的方法。

import { Injectable } from '@nestjs/common';
import { UpdateUserDto } from './dto/update-user.dto';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from '../entities/User.entity';

import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
// 测试加密
import { Encrypt } from 'src/utils/crypto'
// import { User } from 'src/entities/User.entity';

@Injectable()
export class UserService {
  // 使用InjectRepository装饰器并引入Repository这样就可以使用typeorm的操作了
  // 引入 InjectRepository typeOrm 依赖注入 接受一个实体
  // 引入类型 Repository 接受实体泛型
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) { }

  /**
   * @description: 创建用户
   * @param createUserDto 
   * @returns 
   */
  async create(createUserDto: CreateUserDto) {
    const { userName, loginName, password } = createUserDto;
    const userInfo = await this.findOneByLoginName(loginName);
    if (userInfo.length > 0) {
      return { flag: false, msg: "登录名已存在" }
    }
    const user = new User();
    user.userName = userName;
    user.loginName = loginName;
    user.password = Encrypt(password);
    user.sex = 1;

    return this.userRepository.save(user);
  }

  /**
   * @description: 查询所有用户
   * @param query 
   * @returns 
   */
  async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }) {
    // async findAll(query: { keyWord: string, pageCurrent: number, pageSize: number }): Promise<User[]> {
    const list = await this.userRepository.find({
      where: [
        { loginName: Like(`%${query.keyWord}%`) },
        { userName: Like(`%${query.keyWord}%`) }
      ],
      order: {
        id: "DESC"
      },
      skip: (query.pageCurrent - 1) * query.pageSize,
      take: query.pageSize
    })
    const total = list.length
    return { list, total }
    // return await this.userRepository.query('select * from user');
  }

  /**
   * @description: 查询是否存在此用户,登录名不重复
   * @param loginName 
   * @returns 
   */
  async findOneByLoginName(loginName: string): Promise<User[]> {
    return await this.userRepository.find({
      where: {
        loginName: loginName
      }
    });
  }
  /**
   * @description: 根据id查找用户信息
   * @param id 
   * @returns 
   */
  async findOne(id: number) {
    return await this.userRepository.find({
      where: {
        id: id
      }
    });
  }

  /**
   * @description 更新用户
   * @param id 
   * @param updateUserDto 
   * @returns 
   */
  update(id: number, updateUserDto: UpdateUserDto) {
    return this.userRepository.update(id, updateUserDto);
  }

  /**
   * @description 删除用户
   * @param id 
   * @returns 
   */
  remove(id: number) {
    return this.userRepository.delete(id);
  }
}
// 跨域
  app.enableCors()

使用redis,增加token过期和单点登录

安装依赖

yarn add ioredis

下载redis

下载redishttps://github.com/MSOpenTech/redis/releases

设置密码

到redis目录下,命令行运行:redis-server.exe redis.windows.conf

nestjs能否代替Java nestjs项目_redis_05


另起窗口redis-cli.exe -h 127.0.0.1 -p 6379 查看密码 config get requirepass

设置密码:config set requirepass password

nestjs能否代替Java nestjs项目_mysql_06


再登录有密码的redis: redis-cli -p 6379 -a password

redis-cli.exe -h 127.0.0.1 -p 6379 -a password

项目中引用resid

配置redis

然后创建redis.ts 文件

import { Logger } from "@nestjs/common";
import Redis from "ioredis";

const logger = new Logger("auth.service");
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
const redisOption = {
  host: "127.0.0.1",
  port: 6379,
  password: "password",
};
export class RedisInstance {
  static async initRedis(method: string, db = 0) {
    const isExist = redisIndex.some((x) => x === db);
    if (!isExist) {
      Logger.debug(
        `[Redis ${db}]来自 ${method} 方法调用 `
      );
      redisList[db] = new Redis({ ...redisOption, db });
      redisIndex.push(db);
    } else {
      Logger.debug(`[Redis ${db}]来自 ${method} 方法调用`);
    }
    return redisList[db];
  }

  static async setRedis(
    method: string,
    db = 0,
    key: string,
    val: string,
    timeout = 60 * 60
  ) {
    if (typeof val == "object") {
      val = JSON.stringify(val);
    }
    const redis = await RedisInstance.initRedis(method, db);
    redis.set(`${key}`, val);
    redis.expire(`${key}`, timeout);
  }
  static async getRedis(method: string, db = 0, key: string) {
    return new Promise(async (resolve, reject) => {
      const redis = await RedisInstance.initRedis(method, db);
      redis.get(`${key}`, (err, val) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(val);
      });
    });
  }
}