NestJS build blog system (seven) – use JWT to achieve registration and login

preface

Now that you have some infrastructure in place, such as data persistence, form validation, data return, and documentation, you can have a little fun developing your business. This chapter will realize simple registration login authentication function, because the current is a simple single-user system, so here is a simple, to be upgraded to multi-user upgrade.

Register login using JWT

There are many articles on JWT that are not covered here

Create USER module

Create the nest G mo modules/user Create the controller Nest G co modules/user Create the service Nest G S modules/user Create the entity

// src/modules/user/entity/user.entity.ts

import { 
  Entity, 
  Column, 
  PrimaryGeneratedColumn, 
  UpdateDateColumn,
  CreateDateColumn,
  VersionColumn,
} from 'typeorm';

@Entity(a)export class User {
  / / the primary key id
  @PrimaryGeneratedColumn(a)id: number;

  // Create time
  @CreateDateColumn(a)createTime: Date

  // Update time
  @UpdateDateColumn(a)updateTime: Date

  / / soft delete
  @Column({
    default: false
  })
  isDelete: boolean

  // Number of updates
  @VersionColumn(a)version: number

  / / nickname
  @Column('text')
  nickname: string;

  / / cell phone number
  @Column('text')
  mobile: string;

 // Encrypted password
  @Column('text', { select: false })
  password: string;

  / / encryption salt
  @Column('text', { select: false })
  salt: string;
}

Copy the code

Introduce entity in userModule

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

Copy the code

About Registration

Registration is equivalent to the addition of the user module. Login is to issue tokens to the user when the account password is submitted consistent with the database. When accessing resources, the token is put on the request header, so that we can authorize and intercept the access to resources.

Login requires account password verification. From the user’s perspective, account 123 and password ABC are the plaintext 123 and ABC submitted by the user. The service only needs to verify whether 123 corresponds to ABC. You can also encrypt passwords and verify the encrypted passwords. Therefore, we should not use the same account password everyday, because once one site is saved in plain text, it means that all account passwords are leaked.

For encryption is also divided into symmetric encryption and asymmetric encryption, this expansion is also very large, here is not launched, there are many online articles.

There are several options for registering and logging in:

  • Plaintext account password: the database is exposed, and the user account password is directly exposed
  • Use symmetric encryption: database exposure, hackers symmetric decryption, user account password directly exposed
  • Use asymmetric encryption: the database is exposed, hackers use rainbow table brute force crack, over time, a large number of user passwords will be exposed
  • Use asymmetric encryption and salt: the database is exposed, hackers use rainbow table brute force crack, over time, a small number of user passwords will be cracked, but the same plaintext password encrypted after the value is the same.
  • Use asymmetric encryption and add random salt: database exposure, hackers use rainbow table violence crack, a small number of user passwords are cracked, but the same plaintext password after the encryption of the value is not the same.

According to the above thinking, we choose the last way.

Implement registration function

First we define the entry and exit parameters of the registration function

// src/modules/user/dto/register.dto.ts

import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString, Matches } from "class-validator"
import { regMobileCN } from "src/utils/regex.util";

export class RegisterDTO {
  @ApiProperty({
    description: 'Mobile number, unique'.example: '13049153466'
  })
  @Matches(regMobileCN, { message: 'Please enter the correct phone number' })
  @IsNotEmpty({ message: 'Please enter your mobile phone number' })
  readonly mobile: string;

  @ApiProperty({
    description: 'Username'.example: "Steven the Dog."
  })
  @IsNotEmpty({ message: 'Please enter user name' })
  @IsString({ message: 'Name must be String'})
  readonly nickname: string;

  @ApiProperty({
    description: 'User password'.example: '123456',})@IsNotEmpty({ message: 'Please enter your password' })
  readonly password: string;

  @ApiProperty({
    description: 'Enter password twice'.example: '123456'
  })
  @IsNotEmpty({ message: 'Please enter your password again' })
  readonly passwordRepeat: string
}
Copy the code
// src/modules/user/vo/user-info.vo.ts

import { ApiProperty } from "@nestjs/swagger";

export class UserInfoItem {
  @ApiProperty({ description: 'user id'.example: 1 })
  id: number;

  @ApiProperty({ description: 'Creation time'.example: '2021-07-21' }) 
  createTime: Date

  @ApiProperty({ description: 'Update Time'.example: '2021-07-21' }) 
  updateTime: Date

  @ApiProperty({ description: 'Mobile phone Number'.example: '13088888888' }) 
  mobile: string;
}

export class UserInfoVO {
  @ApiProperty({ type: UserInfoItem })
  info: UserInfoItem
}

export class UserInfoResponse {
  @ApiProperty({ description: 'Status code'.example: 200,})code: number

  @ApiProperty({ description: 'data'.type: () = > UserInfoVO, example: UserInfoVO, })
  data: UserInfoVO

  @ApiProperty({ description: 'Request result information'.example: 'Request successful' })
  message: string
} 
Copy the code

Modify the userController

// src/modules/user/user.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { ApiBody, ApiOkResponse } from '@nestjs/swagger';
import { RegisterDTO } from './dto/register.dto';
import { UserService } from './user.service';
import { UserInfoResponse } from './vo/user-info.vo';

@Controller('user')
export class UserController {
  constructor(
    private userService: UserService
  ) {}

  @ApiBody({ type: RegisterDTO })
  @ApiOkResponse({ description: 'registered'.type: UserInfoResponse })
  @Post('register')
  async register(
    @Body() registerDTO: RegisterDTO
  ): Promise<UserInfoResponse> {
    return this.userService.register(registerDTO)
  }
}
Copy the code

For encryption, we use Crypto

Install dependency yarn add crypto-js@types /crypto-js

Create a new tool class

// src/utils/cryptogram.util.ts

import * as crypto from 'crypto';

/ / random salt
export function makeSalt() :string {
  return crypto.randomBytes(3).toString('base64');
}

/** * Use salt to encrypt plaintext passwords *@param "Password," password *@param Salt */
export function encryptPassword(password: string, salt: string) :string {
  if(! password || ! salt) {return ' ';
  }
  const tempSalt = Buffer.from(salt, 'base64');
  return (
    // 10000 represents the number of iterations. 16 represents the length
    crypto.pbkdf2Sync(password, tempSalt, 10000.16.'sha1').toString('base64')); }Copy the code

Service adds a registration method

// src/modules/user/user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { encryptPassword, makeSalt } from 'src/utils/cryptogram.util';
import { Repository } from 'typeorm';
import { RegisterDTO } from './dto/register.dto';
import { User } from './entity/user.entity';

@Injectable(a)export class UserService {

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ){}

  / / register
  async register(
    registerDTO: RegisterDTO
  ): Promise<any> {

    const { nickname, password, mobile } = registerDTO;
    const salt = makeSalt(); // Make password salt
    const hashPassword = encryptPassword(password, salt);  // Encrypt the password

    const newUser: User = new User()
    newUser.nickname = nickname
    newUser.mobile = mobile
    newUser.password = hashPassword 
    newUser.salt = salt
    return await this.userRepository.save(newUser)
  }
}
Copy the code

Open Swagger to test the registration function, but there is no restriction here, so the registration can be repeated

I’m going to add a check

// src/modules/user/user.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { encryptPassword, makeSalt } from 'src/utils/cryptogram.util';
import { Repository } from 'typeorm';
import { RegisterDTO } from './dto/register.dto';
import { User } from './entity/user.entity';

@Injectable(a)export class UserService {

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ){}

  // Verify registration information
  async checkRegisterForm(
    registerDTO: RegisterDTO,
  ): Promise<any> {if(registerDTO.password ! == registerDTO.passwordRepeat) {throw new NotFoundException('The passwords entered twice are inconsistent, please check')}const { mobile } = registerDTO
    const hasUser = await this.userRepository.findOne({ mobile })
    if (hasUser) {      
      throw new NotFoundException('User already exists')}}/ / register
  async register(
    registerDTO: RegisterDTO
  ): Promise<any> {

    await this.checkRegisterForm(registerDTO)

    const { nickname, password, mobile } = registerDTO;
    const salt = makeSalt(); // Make password salt
    const hashPassword = encryptPassword(password, salt);  // Encrypt the password

    const newUser: User = new User()
    newUser.nickname = nickname
    newUser.mobile = mobile
    newUser.password = hashPassword 
    newUser.salt = salt
    return await this.userRepository.save(newUser)
  }

}
Copy the code

At this point, the registration is complete

Log in and issue the TOKEN

The login process is like this:

  1. The user submits the login interface, including mobile password
  2. The service queries user information through mobile and uses password to check whether the password is correct
  3. If the authentication succeeds, the token is generated using the user information
  4. Returns the token

Defining the login interface

// src/modules/user/dto/login.dto.ts

import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, Matches } from "class-validator"
import { regMobileCN } from "src/utils/regex.util";

export class LoginDTO {
  @ApiProperty({
    description: 'Mobile number, unique'.example: '13049153466'
  })
  @Matches(regMobileCN, { message: 'Please enter the correct phone number' })
  @IsNotEmpty({ message: 'Please enter your mobile phone number' })
  readonly mobile: string;

  @ApiProperty({
    description: 'User password'.example: '123456',})@IsNotEmpty({ message: 'Please enter your password' })
  readonly password: string;
}
Copy the code
// src/modules/user/vo/token.vo.ts

import { ApiProperty } from "@nestjs/swagger";

export class TokenItem {
  @ApiProperty({ description: 'token'.example: 'sdfghjkldasascvbnm' }) 
  token: string;
}

export class TokenVO {
  @ApiProperty({ type: TokenItem })
  info: TokenItem
}

export class TokenResponse {
  @ApiProperty({ description: 'Status code'.example: 200,})code: number

  @ApiProperty({ description: 'data'.type: () = > TokenVO, example: TokenVO, })
  data: TokenVO

  @ApiProperty({ description: 'Request result information'.example: 'Request successful' })
  message: string
} 

Copy the code

Add the login method to userController

import { Body, Controller, Post } from '@nestjs/common';
import { ApiBody, ApiOkResponse } from '@nestjs/swagger';
import { LoginDTO } from './dto/login.dto';
import { RegisterDTO } from './dto/register.dto';
import { UserService } from './user.service';
import { TokenResponse } from './vo/token.vo';
import { UserInfoResponse } from './vo/user-info.vo';

@Controller('user')
export class UserController {
  constructor(
    private userService: UserService
  ) {}

  @ApiBody({ type: RegisterDTO })
  @ApiOkResponse({ description: 'registered'.type: UserInfoResponse })
  @Post('register')
  async register(
    @Body() registerDTO: RegisterDTO
  ): Promise<UserInfoResponse> {
    return this.userService.register(registerDTO)
  }

  @ApiBody({ type: LoginDTO })
  @ApiOkResponse({ description: 'login'.type: TokenResponse })
  @Post('login')
  async login(
    @Body() loginDTO: LoginDTO
  ): Promise<any> {
    return this.userService.login(loginDTO)
  }
}
Copy the code

Verifying User Information

// src/modules/user/user.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { encryptPassword, makeSalt } from 'src/utils/cryptogram.util';
import { Repository } from 'typeorm';
import { LoginDTO } from './dto/login.dto';
import { RegisterDTO } from './dto/register.dto';
import { User } from './entity/user.entity';
import { TokenVO } from './vo/token.vo';
import { JwtService } from '@nestjs/jwt';

@Injectable(a)export class UserService {

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,

    private readonly jwtService: JwtService
  ){}

  // Verify registration information
  async checkRegisterForm(
    registerDTO: RegisterDTO,
  ): Promise<any> {if(registerDTO.password ! == registerDTO.passwordRepeat) {throw new NotFoundException('The passwords entered twice are inconsistent, please check')}const { mobile } = registerDTO
    const hasUser = await this.userRepository
      .createQueryBuilder('user')
      .where('user.mobile = :mobile', { mobile })
      .getOne()
    if (hasUser) {      
      throw new NotFoundException('User already exists')}}/ / register
  async register(
    registerDTO: RegisterDTO
  ): Promise<any> {

    await this.checkRegisterForm(registerDTO)

    const { nickname, password, mobile } = registerDTO;
    const salt = makeSalt();
    const hashPassword = encryptPassword(password, salt);

    const newUser: User = new User()
    newUser.nickname = nickname
    newUser.mobile = mobile
    newUser.password = hashPassword 
    newUser.salt = salt
    const result = await this.userRepository.save(newUser)
    delete result.password
    delete result.salt
    return {
      info: result
    }
  }

  // Login to verify user information
  async checkLoginForm(
    loginDTO: LoginDTO
  ): Promise<any> {
    const { mobile, password } = loginDTO
    const user = await this.userRepository
      .createQueryBuilder('user')
      .addSelect('user.salt')
      .addSelect('user.password')
      .where('user.mobile = :mobile', { mobile })
      .getOne()

    if(! user) {throw new NotFoundException('User does not exist')}const { password: dbPassword, salt } = user
    const currentHashPassword = encryptPassword(password, salt)
    if(currentHashPassword ! == dbPassword) {throw new NotFoundException('Password error')}return user
  }

  async login(
    loginDTO: LoginDTO
  ): Promise<any> {const user = await this.checkLoginForm(loginDTO)
    return {
      info: {
        token
      }
    }
  }
}

Copy the code

Generate and return the token

Install dependencies

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

yarn add -D @types/passport-local @types/passport-jwt

Use the JWT module in userModal

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret: 'dasdjanksjdasd'./ / key
      signOptions: { expiresIn: '8h' }, // Token expires}),].controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}
Copy the code

The token is issued after login

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { encryptPassword, makeSalt } from 'src/utils/cryptogram.util';
import { Repository } from 'typeorm';
import { LoginDTO } from './dto/login.dto';
import { RegisterDTO } from './dto/register.dto';
import { User } from './entity/user.entity';
import { TokenVO } from './vo/token.vo';
import { JwtService } from '@nestjs/jwt';

@Injectable(a)export class UserService {

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,

    private readonly jwtService: JwtService
  ){}

  // Verify registration information
  async checkRegisterForm(
    registerDTO: RegisterDTO,
  ): Promise<any> {if(registerDTO.password ! == registerDTO.passwordRepeat) {throw new NotFoundException('The passwords entered twice are inconsistent, please check')}const { mobile } = registerDTO
    const hasUser = await this.userRepository
      .createQueryBuilder('user')
      .where('user.mobile = :mobile', { mobile })
      .getOne()
    if (hasUser) {      
      throw new NotFoundException('User already exists')}}/ / register
  async register(
    registerDTO: RegisterDTO
  ): Promise<any> {

    await this.checkRegisterForm(registerDTO)

    const { nickname, password, mobile } = registerDTO;
    const salt = makeSalt(); // Make password salt
    const hashPassword = encryptPassword(password, salt);  // Encrypt the password

    const newUser: User = new User()
    newUser.nickname = nickname
    newUser.mobile = mobile
    newUser.password = hashPassword 
    newUser.salt = salt
    const result = await this.userRepository.save(newUser)
    delete result.password
    delete result.salt
    return {
      info: result
    }
  }

  // Login to verify user information
  async checkLoginForm(
    loginDTO: LoginDTO
  ): Promise<any> {
    const { mobile, password } = loginDTO
    const user = await this.userRepository
      .createQueryBuilder('user')
      .addSelect('user.salt')
      .addSelect('user.password')
      .where('user.mobile = :mobile', { mobile })
      .getOne()
    
    console.log({ user })

    if(! user) {throw new NotFoundException('User does not exist')}const { password: dbPassword, salt } = user
    const currentHashPassword = encryptPassword(password, salt);
    console.log({currentHashPassword, dbPassword})
    if(currentHashPassword ! == dbPassword) {throw new NotFoundException('Password error')}return user
  }

  / / token is generated
  async certificate(user: User) {
    const payload = { 
      id: user.id,
      nickname: user.nickname,
      mobile: user.mobile,
    };
    const token = this.jwtService.sign(payload);
    return token
  }

  async login(
    loginDTO: LoginDTO
  ): Promise<any> {const user = await this.checkLoginForm(loginDTO)
    const token = await this.certificate(user)
    return {
      info: {
        token
      }
    }
  }
}

Copy the code

Interface authentication

Adding a Policy File

// src/modules/user/jwt.strategy.ts

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable(a)export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false.secretOrKey: 'dasdjanksjdasd'}); }async validate(payload: any) {
    return { 
      id: payload.id,
      mobile: payload.mobile,
      nickname: payload.nickname, }; }}Copy the code

Reference in userModule

// src/modules/user/user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entity/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret: 'dasdjanksjdasd'./ / key
      signOptions: { expiresIn: '60s' }, // Token expires}),].controllers: [UserController],
  providers: [UserService, JwtStrategy]
})
export class UserModule {}
Copy the code

Add login verification to the list of new and modified deletion methods

// src/modules/atricle/article.controller.ts

import { Controller, Body, Query, Get, Post, UseGuards } from '@nestjs/common';
import { ArticleService } from './article.service';
import { ArticleCreateDTO } from './dto/article-create.dto';
import { ArticleEditDTO } from './dto/article-edit.dto';
import { IdDTO } from './dto/id.dto';
import { ListDTO } from './dto/list.dto';
import { ApiTags, ApiOkResponse, ApiHeader, ApiBearerAuth } from '@nestjs/swagger';
import { ArticleInfoVO, ArticleInfoResponse } from './vo/article-info.vo';
import { ArticleListResponse, ArticleListVO } from './vo/article-list.vo';
import { AuthGuard } from '@nestjs/passport';

@ApiTags('Article module')
@Controller('article')
export class ArticleController {
  constructor(
    private articleService: ArticleService
  ) {}

  @Get('list')
  @ApiOkResponse({ description: 'Article List'.type: ArticleListResponse })
  async getMore(
    @Query() listDTO: ListDTO,
  ): Promise<ArticleListVO> {
    return await this.articleService.getMore(listDTO)
  }

  @Get('info')
  @ApiOkResponse({ description: 'Article Details'.type: ArticleInfoResponse })
  async getOne(
    @Query() idDto: IdDTO
  ): Promise<ArticleInfoVO>{
    return await this.articleService.getOne(idDto)
  }

  @UseGuards(AuthGuard('jwt'))
  @Post('create')
  @ApiBearerAuth(a)@ApiOkResponse({ description: 'Create article'.type: ArticleInfoResponse })
  async create(
    @Body() articleCreateDTO: ArticleCreateDTO
  ): Promise<ArticleInfoVO> {
    return await this.articleService.create(articleCreateDTO)
  }

  @UseGuards(AuthGuard('jwt'))
  @Post('edit')
  @ApiBearerAuth(a)@ApiOkResponse({ description: 'Edit article'.type: ArticleInfoResponse })
  async update(
    @Body() articleEditDTO: ArticleEditDTO
  ): Promise<ArticleInfoVO> {
    return await this.articleService.update(articleEditDTO)
  }

  @UseGuards(AuthGuard('jwt'))
  @Post('delete')
  @ApiBearerAuth(a)@ApiOkResponse({ description: 'Delete article'.type: ArticleInfoResponse })
  async delete(
    @Body() idDto: IdDTO,
  ): Promise<ArticleInfoVO> {
    return await this.articleService.delete(idDto)
  }
}
Copy the code

Test list and detail interface can be tuned, but add, modify, delete return 401

Added Swagger support for Auth in main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe())
  app.useGlobalInterceptors(new TransformInterceptor())
  app.useGlobalFilters(new HttpExceptionFilter())

  const options = new DocumentBuilder()
    .setTitle('blog-serve')
    .setDescription('Interface Document')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('swagger-doc', app, document);

  await app.listen(3000);
}
bootstrap();
Copy the code

After login, copy the token to Authorize to add the token information to the header of the interface that uses the @APIBearerAuth () decorator

Try the request to create the article again and see that it was created successfully

reference

  • NestJS
  • NestJS Chinese website
  • Code for this section

A series of

  • NestJS build blog system (a) – build a framework
  • NestJS build blog system (2) – write static article CURD
  • NestJS build blog system (3) – using TypeORM+Mysql to achieve data persistence
  • NestJS build blog system (four) – use interceptor, exception filter to achieve a unified return format
  • Use class-validator+ class validator to implement form validation
  • NestJS build blog system (6) – Use Swagger to generate documents