环境信息:Node.js 20.x、@nestjs/jwt 10.x、TypeORM 0.3.x
JWT登录注册后端实现#
无论如何,我们先创建一个简单的数据表结构来保存用户信息,当然首先还是先创建数据库,比如我们就叫做login_test

常见nest项目:
nest n nest_jwt -g -p pnpm先安装相关的包:
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模块
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 }}由于需要数据传输对象,所以我们简单创建两个传输对象LoginUserDto和RegisterUserDto,里面放入简单的属性username和password
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中,同样创建login和register方法,参数使用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}现在我们已经过来简单测试一下,后端有没有验证数据

接下来就是在UserService中实现用户的注册业务了,我们稍微做一下业务处理:如果用户名已经存在就不进行注册,如果不存在再新增这个用户。
密码我们简单使用md5加密一下,导入md5相关包
pnpm add md5 -Spnpm add @types/md5 -Dimport { 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中测试:

数据库中的数据:

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

那接下来当然就是登录了,同样在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

为了验证路由登录处理,我们可以简单模拟两个需要登录之后才能访问的路由
@Get('info')getUserInfo() { return '获取用户详细信息';}
@Get('list')getUserList() { return '获取用户列表';}如果我们不做限制,当然这两个路由可以直接访问到,现在我们要求必须登录之后才能访问,也就是用户请求头信息的Authorization属性中必须要带有正确的Jwt信息才能访问到这两个路由,我们可以使用守卫来处理这个问题
所以,首先我们创建一个登录守卫:
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 '获取用户列表';}现在如果我们还是直接访问这两个路由,就会抛出登录失效的异常

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