目录
1. 安装依赖并配置 JWT
首先,安装@nestjs/jwt包:
pnpm add @nestjs/jwt -S然后在.env文件中配置一下JWT的密钥与过期时间
# JWT配置JWT_SECRET = bacdefgJWT_EXP = 2h接着,在app.module.ts中导入JwtModule并做配置:
import { Module } from '@nestjs/common'import { AppController } from './app.controller'import { AppService } from './app.service'import { UserModule } from './user/user.module'import { TypeOrmModule } from '@nestjs/typeorm'import { ConfigModule } from '@nestjs/config'import { JwtModule } from '@nestjs/jwt'import { UserGuard } from './user/user.guard'
@Module({ imports: [TypeOrmModule.forRoot({ type: 'mysql', // 数据库类型 host: 'localhost', // 数据库主机 port: 3306, // 数据库端口 username: 'root', // 数据库用户名 password: '123456', // 数据库密码 database: 'nest_demo', // 数据库名称 entities: [__dirname + '/**/*.entity{.ts,.js}'], // 数据库实体文件 synchronize: true, // 是否自动同步实体 // logging: true, // 是否打印日志 retryDelay: 500, // 重试连接间隔 retryAttempts: 10, // 重试连接次数 autoLoadEntities: true, // 自动加载实体
}), ConfigModule.forRoot({ // 加载特定环境的 .env 文件 envFilePath: `.env`, // 使配置服务在所有模块中可用 isGlobal: true, }), JwtModule.registerAsync({ global:true, // 是否全局注册模块 useFactory: async () => { return { secret: process.env.JWT_SECRET, // 密钥 signOptions: { expiresIn: process.env.JWT_EXP }, // 过期时间 } } }), UserModule], controllers: [AppController], providers: [AppService],})export class AppModule { }2. 实现登录功能
在user.service.ts中实现用户登录,校验用户信息并生成JWT token:
import { User } from './entities/user.entity';import { Injectable } from '@nestjs/common';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { hash, verify } from '../utils/md5'import { JwtService } from '@nestjs/jwt'
@Injectable()export class UserService { // 注释:这里使用@InjectRepository 注解,注入User实体,使用Repository<User> constructor( @InjectRepository(User) private usersRepository: Repository<User>, private jwtService: JwtService, ) { }
// 登录 async login(loginDto: CreateUserDto) { const { username, password } = loginDto const user = await this.findByUsername(username) if (!user) return '用户不存在' if (!verify(password, user.password, process.env.MD5_SALT)) { return '密码错误' } const payload = { username: user.username, id: user.id }; return await this.jwtService.signAsync(payload); // 生成token }
// 根据用户名查询用户 async findByUsername(username: string) { // const user = await this.usersRepository.findOneBy({ username }) const user = await this.usersRepository.findOne({ where: { username }, select: ['id', 'username', 'password'] }) return user }}登录成功后,返回的token如下:
{ "code": 200, "success": "请求成功", "timestamp": "2024/9/15 23:38:31", "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}3. 导航守卫 Guard 实现
导航守卫用于拦截路由导航,检查用户是否已登录。
通过nest g gu user生成守卫user.guard.ts:
规定前端请求头字段为authorization,并且以Bearer ${token}(约定俗称)这种形式传值,将guard改为
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'import { JwtService } from '@nestjs/jwt'import { Reflector } from '@nestjs/core'
@Injectable()export class UserGuard implements CanActivate { constructor( private jwtService: JwtService, private reflector: Reflector, ) { }
/** * 判断请求是否通过身份验证 * @param context 执行上下文 * @returns 是否通过身份验证 */ async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); // 获取请求对象 const token = this.extractTokenFromHeader(request); // 从请求头中提取token if (!token) { throw new HttpException('验证不通过', HttpStatus.FORBIDDEN); // 如果没有token,抛出验证不通过异常 } try { const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET, // 使用JWT_SECRET解析token }); request['user'] = payload; // 将解析后的用户信息存储在请求对象中 } catch { throw new HttpException('token验证失败', HttpStatus.FORBIDDEN); // token验证失败,抛出异常 }
return true; // 身份验证通过 }
/** * 从请求头中提取token * @param request 请求对象 * @returns 提取到的token */ private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = (request.headers as { authorization?: string }).authorization?.split(' ') ?? []; // 从Authorization头中提取token return type === 'Bearer' ? token : undefined; // 如果是Bearer类型的token,返回token;否则返回undefined }}测试守卫
在user.module.ts中将UserGuard配置为全局守卫:
import { Controller, Post, Body, UseGuards } from '@nestjs/common';import { UserGuard } from './user.guard';@Controller('user')export class AuthController { constructor(private readonly authService: AuthService) {}
//省略部分代码 @UseGuards(UserGuard) @Post('test') test() { return 1; }}请求结果:
{ "success": false, "timestamp": "2024/9/15 23:54:33", "message": "验证不通过", "code": 403, "path": "/api/v1/user/3"}配置全局守卫
现在我们注册成全局守卫,在user.module.ts引入APP_GUARD并将UserGuard注入,这样它就成为了全局守卫
//省略部分代码import { APP_GUARD } from "@nestjs/core";import { UserGuard } from "./user.guard";@Module({ controllers: [UserController], providers: [ UserService, { provide: APP_GUARD, useClass: UserGuard, }, ], imports: [TypeOrmModule.forFeature([User]), CacheModule],})export class UserModule {}自定义装饰器:免登录访问
这时候将user.controller.ts中的装饰器@UseGuards去掉
发现守卫依然有效
在业务中并不是所有接口都需要验证,比如登录接口,我们可以通过自定义装饰器设置元数据来处理
执行nest g d public生成一个public.decorator.ts创建一个装饰器设置一下元数据isPublic为true
import { SetMetadata } from '@nestjs/common'
export const Public = () => SetMetadata('isPublic', true)然后在user.guard.ts中通过Reflector取出当前的isPublic,如果为true(即用了@Public进行装饰过),则不再进行token判断直接放行
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'import { JwtService } from '@nestjs/jwt'import { Reflector } from '@nestjs/core'
@Injectable()export class UserGuard implements CanActivate { constructor( private jwtService: JwtService, private reflector: Reflector, ) { }
/** * 判断请求是否通过身份验证 * @param context 执行上下文 * @returns 是否通过身份验证 */ async canActivate(context: ExecutionContext): Promise<boolean> { const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ //即将调用的方法 context.getHandler(), //controller类型 context.getClass(), ]) if (isPublic) { return true; } // 省略其他代码}在不需要验证的接口上添加@Public()装饰器:
import { Controller, Get } from '@nestjs/common';import { AppService } from './app.service';import { Public } from './public/public.decorator';
@Controller()export class AppController { constructor(private readonly appService: AppService) {}
@Public() @Get() getHello(): string { return this.appService.getHello(); }}返回结果如下
{ "code": 200, "success": "请求成功", "timestamp": "2024/9/16 00:04:17", "data": "Hello World!"}4. 获取当前用户信息
执行nest g d user生成user.decorator.ts,实现一个用于获取当前用户信息的装饰器:
import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common';
/** * 获取当前用户信息 * @param data 获取用户信息中的字段 key 值 例如:id username */export const User = createParamDecorator((data: string, ctx: ExecutionContext) => { return ctx.switchToHttp().getRequest().user[data];})在user.controller.ts中添加接口获取用户信息:
import { Controller, Get, UseGuards } from '@nestjs/common'import { User } from './user.decorator';@Controller('user')export class AuthController { constructor(private readonly authService: AuthService) {}
@Get('test') test(@User('username') username: string) { return username; // 返回用户名 }}总结
本文介绍了如何在 NestJS 中通过 JwtModule 配置 JWT 认证,使用全局守卫拦截路由,并自定义装饰器获取用户信息或处理免登录路由。通过这种方式,我们可以很方便地实现用户认证和路由保护,确保应用的安全性和灵活性。