Skip to content

JWT登录注册后端处理

环境信息:Node.js 20.x、@nestjs/jwt 10.x、TypeORM 0.3.x

JWT登录注册后端实现#

无论如何,我们先创建一个简单的数据表结构来保存用户信息,当然首先还是先创建数据库,比如我们就叫做login_test

image-20250208142204415

常见nest项目:

Terminal window
nest n nest_jwt -g -p pnpm

先安装相关的包:

Terminal window
pnpm add typeorm mysql2 @nestjs/typeorm @nestjs/jwt -S

然后首先在AppModule中配置TypeORM

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'login_test',
synchronize: true,
logging: true,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
connectorPackage: 'mysql2',
timezone: 'Z',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

然后创建User模块

Terminal window
nest g res user --no-spec

给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
}

@CreateDateColumn@UpdateDateColumn 都是 datetime 类型。

@CreateDateColumn 会在第一次保存的时候设置一个时间戳,之后一直不变。而 @UpdateDateColumn 则是每次更新都会修改这个时间戳。用来保存创建时间和更新时间很方便。

UserModule 引入 TypeOrm.forFeature 动态模块,传入 User 的 entity,防止我们后面使用Repository的时候忘记了

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

所以,接下来当然是在UserService中先注入Repository

而且其他的方法暂时不需要,直接删除,毕竟我们只是要登录和注册。

@Injectable()
export class UserService {
@InjectRepository(User)
private userRepository: Repository<User>
async login(user: LoginUserDto) {
return user
}
async register(user: RegisterUserDto) {
return user
}
}

由于需要数据传输对象,所以我们简单创建两个传输对象LoginUserDtoRegisterUserDto,里面放入简单的属性usernamepassword

export class RegisterUserDto {
username: string
password: string
}
export class LoginUserDto {
username: string
password: string
}

两个类几乎是一模一样的,为什么要用两个类型呢?

一方面他们本身代码的语义是不一样的,一个用于注册,一个用于登录。

另外,在Controller层,用户登录或者注册我们其实都需要进行相关的数据检测。登录和注册我们检查的需求是不一样的。

比如:注册的时候可能需要用户密码不能为空,还会限定长度和特殊字符,而登录的时候就不需要限定的这么详细了,只要限制不为空就行了。

为了达到参数验证的效果,我们可以使用管道 + class-validator来处理

要使用class-validator,需要下载相关的包:

pnpm add class-validator class-transformer

在Controller中,同样创建loginregister方法,参数使用ValidationPipe管道进行验证

import { Body, Controller, Post, ValidationPipe } from '@nestjs/common'
import { UserService } from './user.service'
import { LoginUserDto } from './dto/login-user.dto'
import { RegisterUserDto } from './dto/register-user.dto'
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
login(@Body(ValidationPipe) user: LoginUserDto) {
console.log(user)
return this.userService.login(user)
}
@Post('register')
register(@Body(ValidationPipe) user: RegisterUserDto) {
console.log(user)
return this.userService.register(user)
}
}

两个DTO传输类,需要做到相关验证

import { IsNotEmpty, IsString, Length, Matches } from 'class-validator'
export class RegisterUserDto {
@IsString()
@IsNotEmpty()
@Length(6, 20)
@Matches(/^[A-Za-z0-9_-]+$/, {
message: '用户名只能包含字母、数字、下划线和破折号',
})
username: string
@IsString()
@IsNotEmpty()
@Length(6, 20)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d_%$]+$/, {
message:
'密码只能包含字母、数字和特殊字符_、%、$, 并且至少包含大小写字母和数字',
})
password: string
}
import { IsNotEmpty } from 'class-validator'
export class LoginUserDto {
@IsNotEmpty()
username: string
@IsNotEmpty()
password: string
}

现在我们已经过来简单测试一下,后端有没有验证数据

image-20250208152959774

接下来就是在UserService中实现用户的注册业务了,我们稍微做一下业务处理:如果用户名已经存在就不进行注册,如果不存在再新增这个用户。

密码我们简单使用md5加密一下,导入md5相关包

pnpm add md5 -S
pnpm add @types/md5 -D
import { HttpException, Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { User } from './entities/user.entity'
import { Repository } from 'typeorm'
import { LoginUserDto } from './dto/login-user.dto'
import { RegisterUserDto } from './dto/register-user.dto'
import * as md5 from 'md5'
@Injectable()
export class UserService {
@InjectRepository(User)
private userRepository: Repository<User>
async login(user: LoginUserDto) {
return user
}
async register(user: RegisterUserDto) {
// 查询用户名在数据库中是否存在
const userEntity = await this.userRepository.findOneBy({
username: user.username,
})
if (userEntity) {
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) {
console.log(e, '注册失败')
return '注册失败'
}
}
}

这样在ApiFox中测试:

image-20250208155004983

数据库中的数据:

image-20250208155058102

当用户名已经存在的情况下:

image-20250208155206536

那接下来当然就是登录了,同样在UserService中进行业务处理

@Injectable()
export class UserService {
@InjectRepository(User)
private userRepository: Repository<User>
// 根据用户名查询用户
async findOneByUsername(username: string) {
const user = await this.userRepository.findOneBy({
username,
})
return user
}
// 登录
async login(user: LoginUserDto) {
const loginUser = await this.findOneByUsername(user.username)
if (!loginUser) {
throw new HttpException('用户名不存在', 400)
}
if (loginUser.password !== md5(user.password)) {
throw new HttpException('密码不正确', 400)
}
return loginUser
}
// ......
}

controller中做出相应处理

@Post('login')
async login(@Body(ValidationPipe) user: LoginUserDto) {
const result = await this.userService.login(user);
if (result) {
return {
message: '登录成功',
data: result,
code: 200,
};
} else {
return {
message: '登录失败',
code: 400,
data: null,
};
}
}

登录成功之后我们要把用户信息放在 jwt 或者 session 中,这样后面再请求就知道已经登录了。

在 AppModule 里引入 JwtModule

@Module({
imports: [
// ......
JwtModule.register({
global: true,
secret: 'MySecret',
signOptions: { expiresIn: '7d' },
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

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

接下来要做的事情,就是登录成功之后,把user信息放到jwt通过header返回,在Controller中进行处理,注意需要引入JwtSerevice,并且login方法需要经过修改

// ......
import { JwtService } from '@nestjs/jwt'
import { Response } from 'express'
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Inject(JwtService)
private jwtService: JwtService
@Post('login')
async login(
@Body(ValidationPipe) user: LoginUserDto,
@Res({ passthrough: true }) res: Response
) {
const result = await this.userService.login(user)
if (result) {
const token = await this.jwtService.signAsync({
user: {
id: result.id,
username: result.username,
},
})
res.header('authorization', token)
return {
message: '登录成功',
data: result,
code: 200,
}
} else {
return {
message: '登录失败',
code: 400,
data: null,
}
}
}
// ......
}

这样,再次登录就可以通过heade的authorization,返回Jwt

image-20250210121135272

为了验证路由登录处理,我们可以简单模拟两个需要登录之后才能访问的路由

@Get('info')
getUserInfo() {
return '获取用户详细信息';
}
@Get('list')
getUserList() {
return '获取用户列表';
}

如果我们不做限制,当然这两个路由可以直接访问到,现在我们要求必须登录之后才能访问,也就是用户请求头信息的Authorization属性中必须要带有正确的Jwt信息才能访问到这两个路由,我们可以使用守卫来处理这个问题

所以,首先我们创建一个登录守卫:

Terminal window
nest g guard login --no-spec --flat

处理登录守卫逻辑

import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Observable } from 'rxjs'
import { Request } from 'express'
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
// 通过ExecutionContext获取请求对象,注意这里的Request是express的Request
const request: Request = context.switchToHttp().getRequest()
// 从请求头中获取authorization字段
const authorization = request.header('authorization') || ''
const bearer = authorization.split(' ')
// 如果没有bearer或者bearer不合法,抛出异常
if (!bearer || bearer.length < 2) {
throw new UnauthorizedException('登录失效,请重新登录')
}
const token = bearer[1]
try {
const info = this.jwtService.verify(token)
;(request as any).user = info.user
return true
} catch (e) {
console.log(e)
throw new UnauthorizedException('登录失效,请重新登录')
}
}
}

在Controller中引用这个守卫

@Get('info')
@UseGuards(LoginGuard)
getUserInfo() {
return '获取用户详细信息';
}
@Get('list')
@UseGuards(LoginGuard)
getUserList() {
return '获取用户列表';
}

现在如果我们还是直接访问这两个路由,就会抛出登录失效的异常

image-20250210122804490

现在我们手动记录一下登录的Jwt,然后添加到访问的头信息中

image-20250210122918899