NestJS + JWT :打造全栈认证系统的最佳实践

1,401 阅读7分钟

从零到一:手把手带你用 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 进行密码哈希和验证 完整的登录流程和错误处理 技术亮点与最佳实践

  1. 安全性设计 项目在安全性方面做了多重保障: 密码存储:使用 Argon2 算法哈希密码,比传统的 bcrypt 更安全 参数校验:使用 DTO 和管道进行严格的参数校验,防止注入攻击 JWT 认证:使用 JWT 进行无状态认证,并设置合理的过期时间 验证码:登录注册时使用验证码防止暴力破解
  2. 模块化与解耦 使用 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 定义和管道校验,减轻服务层负担
  • 采用接口与实现分离的方式,提高代码可测试性

项目优化建议

通过开发和重构,我总结了几点优化建议:

  1. 进一步分离关注点:
  • 将验证相关逻辑抽象为 AuthService
  • 将用户相关逻辑抽象为 UserService
  • 让 LoginService 专注于登录流程协调
  1. 增强安全性:
  • 增加登录尝试次数限制,防止暴力破解
  • 实现登录日志记录,便于安全审计
  • 考虑实现 OAuth2 等第三方登录
  1. 提升用户体验:
  • 实现 token 自动刷新机制
  • 多端登录控制
  • 实现"记住我"功能

总结

通过这个项目,我们实现了一个功能完整、安全可靠的用户认证系统。NestJS 的模块化设计和装饰器模式大大提高了开发效率,Prisma 简化了数据库操作,JWT 提供了可靠的身份验证机制。这个项目不仅适合学习 NestJS 和后端认证原理,也可以作为实际项目的认证模块使用。通过合理的架构设计和安全实践,我们构建了一个既安全又可扩展的认证系统。你也在学习 NestJS 或者在构建自己的认证系统?有什么疑问或者改进建议?欢迎在评论区分享你的想法!如果这篇文章对你有帮助,别忘了点赞关注,我们下期再见!


本文代码已上传 GitHub,完整源码请访问:nestjs-auth-projek