从零到一:手把手带你用 NestJS + Prisma + JWT 打造安全可靠的用户认证系统!NestJS认证系统封面
前言
大家好,我是一名前端开发者,最近在学习和实践后端开发,用 NestJS 框架构建了一个完整的用户认证系统。这个系统不仅实现了基础的登录注册功能,还整合了 JWT 认证、验证码校验等安全特性。今天就跟大家分享一下这个项目的开发过程和核心代码实现,希望对大家有所帮助!
项目地址: nest+prisma 如果有用请给一下 star
技术栈介绍
本项目主要使用了以下技术:
- 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 和后端认证原理,也可以作为实际项目的认证模块使用。通过合理的架构设计和安全实践,我们构建了一个既安全又可扩展的认证系统。你也在学习 NestJS 或者在构建自己的认证系统?有什么疑问或者改进建议?欢迎在评论区分享你的想法!如果这篇文章对你有帮助,别忘了点赞关注,我们下期再见!
本文代码已上传 GitHub,完整源码请访问:nestjs-auth-projek