热搜:前端 nest neovim nvim

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

lxf2023-06-26 04:18:21

最近在学习神光大神的《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 生效了。

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

最后,你可以写一部分前端代码,来跑通登录注册前后端联调的过程。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!