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:
- The user submits the login interface, including mobile password
- The service queries user information through mobile and uses password to check whether the password is correct
- If the authentication succeeds, the token is generated using the user information
- 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