NestJS 中实现完整的 JWT 认证系统

2025-08-16 2102字

在现代 Web 开发中,JWT(JSON Web Token)认证因其简单、无状态和可扩展的特性,已成为构建安全 API 的首选方案。特别是在分布式系统和微服务架构中,JWT 的优势更加明显。本文将带你一步步在 NestJS 框架中实现一个完整的 JWT 认证系统。

环境准备与依赖安装

首先,我们需要安装必要的依赖包。NestJS 提供了官方的 JWT 模块 @nestjs/jwt,它封装了常用的 JWT 操作,让我们的开发更加便捷。

npm install @nestjs/jwt

提示:在实际项目中,通常会配合 @nestjs/passport 来实现更复杂的认证策略。但为了让大家更好地理解 JWT 认证的原理,本文将采用手动实现的方式。

配置 JWT 模块

安装完成后,我们需要在应用中注册 JWT 服务。NestJS 的 JwtModule 提供了两种注册方式:同步注册和异步注册。

同步注册(适用于开发环境)

在开发阶段,我们可以使用同步注册方式快速配置:

// app.module.ts

import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.register({
      secret: 'your-secret-key',
      signOptions: { expiresIn: '60s' },
      global: true, // 设置为全局模块
    }),
  ],
})
export class AppModule {}

这种方式简单直接,但存在安全隐患——密钥硬编码在代码中。

异步注册(推荐用于生产环境)

在生产环境中,我们应该使用异步注册方式,从环境变量中读取配置:

// app.module.ts

import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET', 'jwt_secret_key'),
        signOptions: { 
          expiresIn: configService.get('JWT_EXPIRES_IN', '1d') 
        },
      }),
      global: true,
    }),
  ],
})
export class AppModule {}

通过 ConfigService 从环境变量中读取配置,不仅提高了安全性,还让配置管理更加灵活。

生成 JWT Token

配置好 JWT 模块后,我们就可以在业务逻辑中生成 Token 了。通常在用户登录成功后,我们需要生成一个包含用户信息的 JWT Token。

// user.service.ts

import { JwtService } from '@nestjs/jwt';

@Injectable()
export class UserService {
  constructor(private readonly jwtService: JwtService) {}

  async login(user: User) {
    // 构建 Token 负载(payload)
    const payload = { 
      userId: user.id, 
      username: user.username, 
      email: user.email 
    };
    
    // 生成 JWT Token
    const token = this.jwtService.sign(payload);
    
    return { 
      token,
      user: {
        id: user.id,
        username: user.username,
        email: user.email
      }
    };
  }
}

关键点说明:

Token 认证流程与前端集成

先看下完整的 JWT 认证流程:

  1. 用户登录:前端发送用户名密码到登录接口
  2. 生成 Token:服务器验证成功后生成 JWT Token
  3. 返回 Token:服务器将 Token 返回给前端
  4. 存储 Token:前端将 Token 存储在 localStorage 或 cookie 中
  5. 携带 Token:后续请求在 Authorization 头中携带 Token
  6. 验证 Token:服务器验证 Token 有效性

前端如何携带 Token?

在前端应用中,我们需要在每次请求的 Authorization 头中携带 Token:

import axios from 'axios';

// 从存储中获取 Token
const token = localStorage.getItem('jwt_token');

// 发送请求时携带 Token
axios.get('/api/protected-endpoint', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

注意事项:

实现 Token 校验与路由守卫

现在我们来解决最关键的问题:如何在服务器端验证 Token 的有效性。如果为每个需要认证的接口都编写校验代码,显然是不现实的。NestJS 提供了优雅的解决方案——路由守卫(Guard)。

什么是路由守卫?

路由守卫是 NestJS AOP(面向切面编程)思想的重要体现。它可以在请求到达控制器之前进行拦截,执行一些预处理逻辑,比如权限验证、Token 校验等。

创建认证守卫

让我们创建一个 AuthGuard 来实现 JWT Token 的校验:

// auth.guard.ts

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import type { Request } from 'express';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const token = this.extractTokenFromHeader(request);
    
    // 检查 Token 是否存在
    if (!token) {
      throw new UnauthorizedException('Token 不存在');
    }
    
    try {
      // 验证 Token 有效性
      const payload = await this.jwtService.verifyAsync(token);
      
      // 将用户信息挂载到请求对象上,供后续使用
      request['user'] = payload;
    } catch (error) {
      throw new UnauthorizedException('Token 无效或已过期');
    }

    return true;
  }

  /**
   * 从请求头中提取 Token
   */
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

守卫的核心功能:

  1. 提取 Token:从 Authorization 头中提取 Bearer Token
  2. 验证有效性:使用 JwtService.verifyAsync() 验证 Token
  3. 挂载用户信息:验证成功后,将用户信息挂载到请求对象
  4. 异常处理:Token 无效或过期时抛出相应异常

使用认证守卫

守卫的使用非常简单,只需要在需要保护的接口或控制器上添加 @UseGuards() 装饰器:

// user.controller.ts

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 保护单个接口
  @UseGuards(AuthGuard)
  @Get('profile')
  async getProfile(@Req() request: Request) {
    // 可以直接从请求对象中获取用户信息
    const user = request['user'];
    return await this.userService.getProfile(user.userId);
  }
}

控制器级别的守卫

如果整个控制器的接口都需要保护,可以在控制器级别应用守卫:

// user.controller.ts

@Controller('user')
@UseGuards(AuthGuard) // 应用到整个控制器
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get('profile')
  async getProfile(@Req() request: Request) {
    const user = request['user'];
    return await this.userService.getProfile(user.userId);
  }

  @Get('settings')
  async getSettings(@Req() request: Request) {
    const user = request['user'];
    return await this.userService.getSettings(user.userId);
  }
}

控制器级别的守卫虽然方便,但会保护所有接口,包括注册、登录等公开接口。为了解决这个问题,我们需要实现一个机制来标记公开接口。

实现公开接口标记机制

为了解决控制器级别守卫的局限性,我们需要实现一个机制来标记公开接口。NestJS 提供了元数据(Metadata)机制,可以让我们在接口上添加自定义标记。

创建公开接口装饰器

首先创建一个自定义装饰器来标记公开接口:

// public.decorator.ts

import { SetMetadata } from '@nestjs/common';

// 定义元数据键名
export const IS_PUBLIC_KEY = 'isPublic';

// 创建公开接口装饰器
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

这个装饰器的作用是在方法或类上设置一个元数据标记,表示该接口不需要 Token 认证。

使用公开接口装饰器

在控制器中使用 @Public() 装饰器标记公开接口:

// user.controller.ts

@Controller('user')
@UseGuards(AuthGuard) // 整个控制器默认需要认证
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Public() // 标记为公开接口
  @Post('register')
  async register(@Body() userRegisterDto: UserRegisterDto) {
    return await this.userService.create(userRegisterDto);
  }

  @Public() // 标记为公开接口
  @Post('login')
  async login(@Body() userLoginDto: UserLoginDto) {
    return await this.userService.login(userLoginDto);
  }

  // 需要认证的接口
  @Get('profile')
  async getProfile(@Req() request: Request) {
    const user = request['user'];
    return await this.userService.getProfile(user.userId);
  }
}

更新认证守卫

现在我们需要修改认证守卫,让它能够识别公开接口标记:

// auth.guard.ts

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import type { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private readonly reflector: Reflector, // 注入 Reflector 服务
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 检查是否为公开接口
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(), // 方法级别
      context.getClass(),   // 类级别
    ]);
    
    // 如果是公开接口,直接放行
    if (isPublic) {
      return true;
    }
    
    // 非公开接口,进行 Token 校验
    const request = context.switchToHttp().getRequest<Request>();
    const token = this.extractTokenFromHeader(request);
    
    if (!token) {
      throw new UnauthorizedException('请提供有效的 Token');
    }
    
    try {
      const payload = await this.jwtService.verifyAsync(token);
      request['user'] = payload;
    } catch (error) {
      throw new UnauthorizedException('Token 无效或已过期');
    }

    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

关键改进:

总结

通过本文的实现,我们成功构建了一个基于 NestJS 的 JWT 认证系统。核心在于利用路由守卫进行 Token 验证,配合自定义装饰器实现灵活的权限控制。这种方案既保证了安全性,又保持了代码的简洁性。

#NestJS #JWT #后端开发