从零到一:手把手带你用 NestJS + Prisma + JWT 打造安全可靠的用户认证系统!NestJS认证系统封面
前言
最近在学习和实践后端开发,用 NestJS 框架构建了一个完整的用户认证系统。这个系统不仅实现了基础的登录注册功能,还整合了 JWT 认证、验证码校验等安全特性。今天就跟大家分享一下这个项目的开发过程和核心代码实现,希望对大家有所帮助!
技术栈介绍
本项目主要使用了以下技术:
-
NestJS: 基于 TypeScript 的服务端框架,提供了优雅的架构模式
-
Prisma: 现代化的 ORM 工具,简化数据库操作
-
JWT: 用于身份验证的 JSON Web Token
-
Argon2: 比 bcrypt 更安全的密码哈希算法
-
svg-captcha: 生成 SVG 格式的验证码
-
class-validator: 请求参数校验
核心功能模块讲解
1. 模块化设计:登录模块结构
首先看一下我们的登录模块结构:
// login.module.ts import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { PrismaService } from 'src/db'; import { LoginController } from './login.controller'; import { LoginPipe } from './login.pipe'; import { LoginService } from './login.service'; import { JwtStrategy } from './jstStrategy'; @Module({ imports: [ JwtModule.registerAsync({ inject: [ConfigService], global: true, useFactory: (configService: ConfigService) => ({ secret: configService.get('TOKEN_SECRET'), signOptions: { expiresIn: '60s' }, }), }), ], controllers: [LoginController], providers: [LoginService, PrismaService, LoginPipe, JwtStrategy], }) export class LoginModule {}这段代码展示了 NestJS 的模块化设计理念。我们通过 @Module 装饰器定义了登录模块,并引入了 JWT 模块来处理令牌生成和验证。特别注意的是: 使用 registerAsync 方法异步配置 JWT,便于从环境变量获取密钥 将 JWT 设置为全局模块,让整个应用都能使用 注入了登录控制器、服务、数据库服务和参数校验管道 > 小技巧:将敏感信息如 Token 密钥放在环境变量中,避免硬编码到代码中,大大提高了项目的安全性。
2. 参数校验:DTO 和验证管道
我们使用 DTO (Data Transfer Object) 来定义和校验请求参数:
// loginAuth.dto.ts import { IsNotEmpty, IsString } from 'class-validator'; export class LoginAuthDto { @IsNotEmpty() @IsString({ message: '用户名不能为空' }) name: string; @IsNotEmpty() @IsString({ message: '密码不能为空' }) password: string; }简单几行代码就完成了参数校验的定义,配合 ValidationPipe 全局管道,可以自动拦截不合法的请求。这种声明式的校验方式比手动在代码中写判断要简洁得多,而且错误提示也更加友好。
3. 控制器层:处理 HTTP 请求
// login.controller.ts 的部分代码 @Controller('auth') export class LoginController { constructor( private readonly loginService: LoginService, private readonly prisma: PrismaService, ) {} @Get('svg') @Header('Content-Type', 'image/svg+xml') async creactSvg(@Req() req: Request) { return this.loginService.creactSvg(req); } @Post('login') async userLogin(@Body() body: any, @Query() query: any, @Req() req: Request) { return await this.loginService.processLogin(body, query); } @Get('all') @Auth() // 自定义认证装饰器 async getAll(@User() user: any) { // 自定义用户信息提取装饰器 console.log('user', user); return this.prisma.user.findMany(); } }控制器层负责接收和响应 HTTP 请求,主要实现了: 生成验证码 SVG 图片 处理用户注册和登录 提供需要认证的 API 接口(使用 @Auth() 自定义装饰器) > 实战经验:使用自定义装饰器可以大大简化代码,提高可读性。如 @Auth() 装饰器替代了直接使用 @UseGuards(),@User() 装饰器则可以直接获取当前登录用户信息。
4. 服务层:核心业务逻辑
服务层包含了最核心的业务逻辑,下面重点看一下用户认证相关的代码:
// login.service.ts 的部分代码 @Injectable() export class LoginService { constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, ) {} // 生成 JWT 令牌 async generateToken({ username, id }: User) { const token = await this.jwtService.signAsync({ username, sub: id }); console.log('生成的token:', token); return token; } // 查找用户并验证密码 async findUserByUsername(body: LoginAuthDto) { const user = await this.prisma.user.findUnique({ where: { name: body.name }, }); if (!user) { throw new UnauthorizedException('用户不存在'); } // 验证密码 if (!(await verify(user.password, body.password))) { throw new UnauthorizedException('密码错误'); } // 返回安全的用户信息(排除密码) const { password, ...result } = user; return result; } // 处理登录逻辑 async processLogin(body: any, query: any) { try { const loginData = { name: body.name || query.name, password: body.password || query.password, }; if (!loginData.name || !loginData.password) { throw new UnauthorizedException('用户名和密码不能为空'); } const user = await this.findUserByUsername(loginData); const token = await this.generateToken({ id: user.id, username: user.name, }); return { success: true, message: '登录成功', code: 200, user, token, }; } catch (error) { // 处理异常 } } }服务层代码实现了: 用 JWT 生成安全令牌 用 Argon2 进行密码哈希和验证 完整的登录流程和错误处理 技术亮点与最佳实践
- 安全性设计 项目在安全性方面做了多重保障: 密码存储:使用 Argon2 算法哈希密码,比传统的 bcrypt 更安全 参数校验:使用 DTO 和管道进行严格的参数校验,防止注入攻击 JWT 认证:使用 JWT 进行无状态认证,并设置合理的过期时间 验证码:登录注册时使用验证码防止暴力破解
- 模块化与解耦 使用 NestJS 的模块系统组织代码,实现了高内聚低耦合 通过依赖注入实现组件解耦,提高代码可测试性 使用装饰器模式简化代码,提高可读性
3. 异常处理
项目实现了全面的异常处理机制:
try { // 业务逻辑 } catch (error) { console.error('登录处理失败:', error.message); if (error instanceof UnauthorizedException) { throw error; // 直接重新抛出认证异常 } // 其他错误转换为通用错误 throw new UnauthorizedException('登录失败,请检查您的凭据'); }这种异常处理方式既保留了错误信息的详细度,又避免了向前端泄露敏感信息。
开发中踩过的坑和解决方案
1. JWT 配置与使用
坑:最初设置 JWT 过期时间太短(60秒),导致用户频繁登出。解决方案:
-
对于生产环境,建议设置更合理的过期时间,如 1 小时或 1 天
-
实现 token 刷新机制,在旧 token 即将过期时自动刷新
// 更合理的 JWT 配置 JwtModule.registerAsync({ useFactory: (configService: ConfigService) => ({ secret: configService.get('TOKEN_SECRET'), signOptions: { expiresIn: process.env.NODE_ENV === 'production' ? '24h' : '60s' }, }), }),2. 验证码存储问题
坑:初期在 Session 中存储验证码时经常丢失,导致验证失败。解决方案:
-
确保正确配置 Session 中间件
-
使用 Redis 等分布式存储验证码,提高可靠性和扩展性
-
添加显式 Session 保存逻辑:
await new Promise((resolve) => { req.session.save(() => { console.log('Session 已保存'); resolve(); }); });3. 代码解耦与优化
坑:初期代码耦合度高,服务层既处理数据库操作,又处理业务逻辑,代码冗长且难以维护。解决方案:
-
按职责分离代码,将用户查询相关的代码抽象为独立服务
-
使用更细粒度的 DTO 定义和管道校验,减轻服务层负担
-
采用接口与实现分离的方式,提高代码可测试性
项目优化建议
通过开发和重构,我总结了几点优化建议:
- 进一步分离关注点:
-
将验证相关逻辑抽象为 AuthService
-
将用户相关逻辑抽象为 UserService
-
让 LoginService 专注于登录流程协调
- 增强安全性:
-
增加登录尝试次数限制,防止暴力破解
-
实现登录日志记录,便于安全审计
-
考虑实现 OAuth2 等第三方登录
- 提升用户体验:
-
实现 token 自动刷新机制
-
多端登录控制
-
实现"记住我"功能
总结
通过这个项目,我们实现了一个功能完整、安全可靠的用户认证系统。NestJS 的模块化设计和装饰器模式大大提高了开发效率,Prisma 简化了数据库操作,JWT 提供了可靠的身份验证机制。这个项目不仅适合学习 NestJS 和后端认证原理,也可以作为实际项目的认证模块使用。通过合理的架构设计和安全实践,我们构建了一个既安全又可扩展的认证系统。
Loading comments...