最近在学习神光大神的《Nest通关秘籍》,接下来的日子里,我将更新一系列的学习笔记

感兴趣的能够重视我的专栏《Nest 通关秘籍》学习总结。

特别声明:本系列文章现已经过作者本人的答应。 咱们也不要想着白嫖,此笔记仅仅个人学习记录,不是非常完善,如想深入学习能够去购买原版小册,购买链接点击《传送门》。

学完了 mysqltypeormjwt/session 之后,今日咱们学习做个归纳实战事例:登录注册。

1. 创立数据库

CREATE SCHEMA login_test DEFAULT CHARACTER SET utf8mb4;

2. 创立项目

nest new login-and-register -p pnpm

3. 安装包

安装 typeorm相关的包:

npm install --save @nestjs/typeorm typeorm mysql2

然后在 AppModule 里引进 TypeOrmModule,传入 option:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'xxxxxx',
      database: 'login_test',
      synchronize: true,
      logging: true,
      entities: [],
      poolSize: 10,
      connectorPackage: 'mysql2',
      extra: {
        authPlugin: 'sha256_password',
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

4. 创立 CRUD 模块

创立个 user 的 CRUD 模块:

nest g resource user

在AppModule中引进 在User 的 entity:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

然后给 User 增加一些属性:

import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;
    @Column({
        length: 50,
        comment: '用户名'
    })
    username: string;
    @Column({
        length:50,
        comment: '暗码'
    })
    password: string;
    @CreateDateColumn({
        comment: '创立时刻'
    })
    createTime: Date;
    @UpdateDateColumn({
        comment: '更新时刻'
    })
    updateTime: Date;
}
  • id 列是主键、自动递加。
  • username 和 password 是用户名和暗码,类型是 VARCHAR(50)。
  • createTime 是创立时刻,updateTime 是更新时刻。
  • @CreateDateColumn 和 @UpdateDateColumn 都是 datetime 类型。@CreateDateColumn 会在第一次保存的时分设置一个时刻戳,之后一直不变。而 @UpdateDateColumn 则是每次更新都会修正这个时刻戳。

运行项目:

nest start --watch

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到打印了 create table 的建表 sql,数据库中也生成了对应的user表和字段。

在 UserModule 引进 TypeOrm.forFeature 动态模块,传入 User 的 entity。

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

这样模块内就能够注入 User 对应的 Repository 了:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

然后就能够完成 User 的增删改查。

咱们在 UserController 里增加两个 handler:

import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  @Post('login')
  login(@Body() user: LoginDto) {
    return user;
  }
  @Post('register')
  register(@Body() user: RegisterDto) {
    return user;
  }
}

这儿的LoginDtoRegisterDto如下:

// login.dto.ts
export class LoginDto {
  username: string;
  password: string;
}
// register.dto.ts
export class RegisterDto {
  username: string;
  password: string;
}

然后咱们在apifox中测验一下:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到,都恳求成功了。

接下来咱们来处理详细的逻辑。首要:

login 和 register 的处理不同:

  • register 是把用户信息存到数据库里
  • login 是依据 username 和 password 取匹配是否有这个 user

先来完成注册功能。

5. 注册

先在user.controller.ts中修正register

@Post('register')
async register(@Body() user: RegisterDto) {
    return await this.userService.register(user);
}

然后在user.service.ts中增加一个register办法:

import { Injectable, HttpException, Logger } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import * as crypto from 'crypto';
function md5(str) {
  const hash = crypto.createHash('md5');
  hash.update(str);
  return hash.digest('hex');
}
@Injectable()
export class UserService {
  private logger = new Logger();
  @InjectRepository(User)
  private userRepository: Repository<User>;
  async register(user: RegisterDto) {
    const foundUser = await this.userRepository.findOneBy({
      username: user.username,
    });
    /**
     * 校验用户是否已存在
     */
    if (foundUser) {
      throw new HttpException('用户已存在', 200);
    }
    const newUser = new User();
    newUser.username = user.username;
    newUser.password = md5(user.password);
    try {
      await this.userRepository.save(newUser);
      return '注册成功';
    } catch (e) {
      this.logger.error(e, UserService);
      return '注册失利';
    }
  }
}

先依据 username 查找下,假如找到了,说明用户已存在,抛一个 HttpException 让 exception filter 处理。

不然,创立 User 目标,调用 userRepository 的 save 办法保存。

password 需要加密,这儿运用 node 内置的 crypto 包来完成。

在apifox里边来测验一下:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到,注册接口恳求成功,而且数据现已保存在数据库中,暗码现已被加密。

以上就是注册逻辑的完成。下面咱们来完成登录接口。

6. 登录

先在user.controller.ts中修正login

@Post('login')
  async login(@Body() user: LoginDto) {
    const foundUser: LoginDto = await this.userService.login(user);
    if (foundUser) {
      return 'login success';
    } else {
      return 'login fail';
    }
  }

然后在user.service.ts中增加一个login办法:

async login(user: LoginDto) {
    const foundUser = await this.userRepository.findOneBy({
      username: user.username,
    });
    if (!foundUser) {
      throw new HttpException('用户名不存在', 200);
    }
    if (foundUser.password !== md5(user.password)) {
      throw new HttpException('暗码过错', 200);
    }
    return foundUser;
  }

依据用户名查找用户,没找到就抛出用户不存在的 HttpException、找到可是暗码不对就抛出暗码过错的 HttpException。不然,回来找到的用户。

咱们来在apifox中测验一下登录接口:

1.账户过错

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

2.暗码过错

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

3.账户暗码都正确

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到,接口都回来了正确的成果。

登录成功今后,咱们要把用户信息放在 jwt 或许 session 中一份,这样后边再恳求就知道现已登录了。

7. jwt鉴权

安装 @nestjs/jwt 的包:

pnpm install @nestjs/jwt

在 AppModule 里引进 JwtModule:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { User } from './user/entities/user.entity';
@Module({
  imports: [
    ...
    JwtModule.register({
      global: true,
      secret: 'xiumubai',
      signOptions: {
        expiresIn: '7d',
      },
    }),
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

global:true 声明为全局模块,这样就不必每个模块都引进它了,指定加密密钥,token 过期时刻。

在 UserController 里注入 JwtService:

import { Body, Controller, Post, Inject, Res } from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  @Inject(JwtService)
  private jwtService: JwtService;
  ....
  @Post('login')
  async login(
    @Body() user: LoginDto,
    @Res({ passthrough: true }) res: Response,
  ) {
    const foundUser: LoginDto = await this.userService.login(user);
    if (foundUser) {
      const token = await this.jwtService.signAsync({
        user: {
          id: foundUser.id,
          username: foundUser.username,
        },
      });
      res.setHeader('authorization', 'bearer ' + token);
      return 'login success';
    } else {
      return 'login fail';
    }
  }
}

用apifox测验一下:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到,token现已拿到了。在这个token中是携带着用户信息的,就是咱们id和username。

现在假如有两个接口,在恳求的时分需要登录,那咱们就需要前端把这个token传过来,然后解析里边的用户信息,看看是否正确。

下面咱们再写一个获取用户信息的接口getUserInfo:

@Get('getUserInfo')
  getUserInfo() {
  return 'userinfo';
}

这个接口现在不需要登录就能够恳求。

现在咱们来增加个 Guard 来限制拜访:

nest g guard login --no-spec --flat
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  Inject,
  UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import { User } from './entities/user.entity';
@Injectable()
export class LoginGuard implements CanActivate {
  @Inject(JwtService)
  private jwtService: JwtService;
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    const authorization = request.header('authorization') || '';
    const bearer = authorization.split(' ');
    if (!bearer || bearer.length < 2) {
      throw new UnauthorizedException('登录 token 过错');
    }
    const token = bearer[1];
    try {
      const info = this.jwtService.verify(token);
      (request as any).user = info.user;
      return true;
    } catch (e) {
      throw new UnauthorizedException('登录 token 失效,请从头登录');
    }
  }
}

取出 authorization 的 header,验证 token 是否有用,token 有用回来 true,无效的话就回来 UnauthorizedException。

把这个 Guard 应用到 getUserInfo

import {
  Body,
  Controller,
  Post,
  Inject,
  Res,
  Get,
  UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginGuard } from './login.guard';
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  ...
  @Get('getUserInfo')
  @UseGuards(LoginGuard)
  getUserInfo() {
    return 'userinfo';
  }
}

接下来,在apifox中再次恳求getUserInfo接口:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

能够看到,这时分没有带token信心,鉴权失利。

当咱们携带一个正确的token再次恳求:

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

这次恳求成功了。

以上。咱们完成了登录注册的流程。

接下来,咱们需要对参数进行校验。

8. 参数校验

安装 class-validator 和 class-transformer 的包:

pnpm install class-validator class-transformer

然后给 /user/login /user/register 接口增加 ValidationPipe:

import {
  Body,
  Controller,
  Post,
  Inject,
  Res,
  Get,
  UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { Response } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginGuard } from './login.guard';
import { ValidationPipe } from '@nestjs/common/pipes';
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  @Inject(JwtService)
  private jwtService: JwtService;
  @Post('register')
  register(@Body(ValidationPipe) user: RegisterDto) {
    return this.userService.register(user);
  }
  @Post('login')
  async login(
    @Body(ValidationPipe) user: LoginDto,
    @Res({ passthrough: true }) res: Response,
  ) {
    const foundUser: LoginDto = await this.userService.login(user);
    if (foundUser) {
      const token = await this.jwtService.signAsync({
        user: {
          id: foundUser.id,
          username: foundUser.username,
        },
      });
      res.setHeader('authorization', 'bearer ' + token);
      return 'login success';
    } else {
      return 'login fail';
    }
  }
  @Get('getUserInfo')
  @UseGuards(LoginGuard)
  getUserInfo() {
    return 'userinfo';
  }
}

在 dto 里声明参数的束缚:

// register.dto.ts
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
/**
 * 注册的时分,用户名暗码不能为空,长度为 6-30,并且限制了不能是特别字符。
 */
export class RegisterDto {
  @IsString()
  @IsNotEmpty()
  @Length(6, 30)
  @Matches(/^[a-zA-Z0-9#$%_-]+$/, {
    message: '用户名只能是字母、数字或许 #、$、%、_、- 这些字符',
  })
  username: string;
  @IsString()
  @IsNotEmpty()
  @Length(6, 30)
  password: string;
}
// login.dto.ts
import { IsNotEmpty } from 'class-validator';
export class LoginDto {
  id?: number;
  @IsNotEmpty()
  username: string;
  @IsNotEmpty()
  password: string;
}

在apifox中测验一下:

咱们下来测验注册接口。

1.测验用户名不合法

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

2.用户名为空

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

这儿命中了好几种规矩

3.用户名长度

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

其他情况咱们自行检测。

4.测验暗码为空

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

5.暗码长度不合法

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

接下来测验一下登录:

1.用户名为空

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

2.暗码为空

神光《Nest 通关秘籍》学习总结--登录注册综合案例实战

ValidationPipe 收效了。

至此,咱们就完成了了登录、注册和鉴权的完整功能,并且在后端增加了参数校验。

最终,你能够写一部分前端代码,来跑通登录注册前后端联调的进程。