NestJS 中使用 JWT Guard 实现 API 鉴权:保护资源操作的安全实践

3 阅读5分钟

在现代后端开发中,API 鉴权是保障资源安全的关键环节。NestJS 作为一款模块化且高效的框架,提供了强大的守卫(Guard)机制,结合 JWT(JSON Web Token) 可以轻松实现用户身份验证。例如,对于新增文章、点赞等需要权限的操作,用户必须先登录,并通过 token 携带身份信息。access_tokenrefresh_token 的双 token 机制:在 axios 请求中自动添加 Authorization 头部,后端通过 Guard 验证 token。如果验证失败,抛出 401 错误;成功则将用户对象附加到请求中,便于后续处理。

双 token 机制的核心在于:access_token 用于日常 API 调用,短效以防泄露;refresh_token 用于刷新新 token 对。鉴权流程包括 token 生成(@nestjs/jwt)、Guard 保护路由(@nestjs/passport)和刷新逻辑。本文将基于一个 Posts 模块的示例,详细讲解 NestJS 中 JWT Guard 的实现。所有示例代码均来源于实际项目实践,帮助大家理解如何在控制器中应用 UseGuards 装饰器,实现安全的资源操作。

鉴权基础:Guard 和 Strategy 的作用

NestJS 的 Guard 是一个装饰器(@UseGuards),应用于控制器或具体路由方法上。它在处理请求前执行验证逻辑,如果失败直接返回错误响应,不会进入方法体。这符合 AOP(面向切面编程)的思想,将鉴权与业务分离。

对于 JWT 鉴权,NestJS 借助 @nestjs/passport 和 passport-jwt 库。Strategy 定义验证规则,如从请求头提取 token、验证签名和 payload。Guard 则调用 Strategy 执行检查。笔记中提到“Unknown authentication strategy 'jwt'”,这是因为需要自定义 JwtStrategy 并在模块中提供。

双 token 流程回顾:

  • 用户登录生成 token 对(access_token 短效,refresh_token 长效)。
  • API 请求携带 access_token。
  • Guard 验证:成功附 user 到 req;失败 401。
  • access_token 过期时,前端用 refresh_token 刷新新对。
  • 示例中,PostsController 的 createPost 方法需保护:只有登录用户才能新增文章。

这种机制确保了操作的安全性:如 post /posts 新增时,如果无 token 或失效,Guard 返回 401。

JwtStrategy:定义 JWT 验证规则

首先,我们自定义 JwtStrategy继承 PassportStrategy。它配置 token 提取方式和 secret。示例代码:

TypeScript

import { Injectable } from '@nestjs/common';
// 定义和继承 Passport 身份验证策略和基类 定规则
import { PassportStrategy } from '@nestjs/passport';
// 身份验证策略选择 jwt
import { Strategy, ExtractJwt } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // token 在哪里 Bearer 前缀 Authorization
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // 不是直接调用 PassportStrategy(Strategy) 封装
      // 自动化地去做
      ignoreExpiration: false,
      secretOrKey: process.env.TOKEN_SECRET || ""
    })
  }

  async validate(payload: any) {
    // console.log(payload);
    return {
      id: payload.sub,
      name: payload.name,
    }
  }
}

剖析:

  • super 配置:jwtFromRequest 使用 ExtractJwt.fromAuthHeaderAsBearerToken() 从 Authorization: Bearer 提取。
  • ignoreExpiration: false 确保检查过期。
  • secretOrKey 从环境变量取,用于验证签名。
  • validate 方法:payload 是 token 解码后的对象(含 sub/id 和 name),返回简化 user 对象,Guard 会附加到 req.user。

需要在 AuthModule 或全局提供这个 Strategy,通常在 providers: [JwtStrategy]。

JwtAuthGuard:继承 AuthGuard 应用策略

Guard 是 Strategy 的执行者。我们自定义 JwtAuthGuard,继承 AuthGuard('jwt'):

TypeScript

import {
  Injectable,
} from '@nestjs/common'

// nestjs 默认提供的guard,自动解析req Authorization
import { AuthGuard } from '@nestjs/passport';

// req header Authorization
// 关注的是 access_token
// @nestjs/jwt verify 
// service 看待 依赖注入
// 继承 AuthGuard 基类
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  
}

这个 Guard 调用 'jwt' Strategy 执行验证。继承后,无需额外逻辑,NestJS 自动处理:提取 token、调用 verify(通过 @nestjs/jwt 内部)、附加 user。

如果模块未提供 JwtStrategy,会抛“Unknown authentication strategy 'jwt'”错误。解决:在 AuthModule imports: [PassportModule],providers: [JwtStrategy]。

PostsController:应用 Guard 保护路由

在控制器中,使用 @UseGuards(JwtAuthGuard) 保护 createPost 方法。完整代码:

TypeScript

import {
  Controller,
  Get,
  Post,
  Query,
  Body,
  UseGuards,
  Req,
} from '@nestjs/common';

import { PostsService } from './posts.service'
import { PostQueryDto } from './dto/post-query.dto'
// auth 模块
import { JwtAuthGuard } from '../auth/guard/jwt-auth.guard';


@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {

  }

  @Get()
  async getPosts(@Query() query: PostQueryDto) {
    console.log(query)
    return this.postsService.findAll(query);
  }


  // 发布文章的处理函数
  // restful
  // post 名词 post 
  @Post()
  @UseGuards(JwtAuthGuard)   // 路由守卫
  createPost(
    @Body("title") title: string,
    @Body("content") content: string,
    @Req() req
  ) {
    // console.log(req.user);
    const { user } = req;
    return this.postsService.createPost({
      title,
      content,
      userId: req.user.id
    })
  }
}

getPosts 无需鉴权,直接查询。createPost 使用 Guard:验证通过后,req.user 可用(从 validate 返回)。传入 userId 到 service 创建帖子。

PostsService:业务逻辑与数据处理

服务实现查询和创建。findAll 支持分页,createPost 创建帖子。代码:

TypeScript

import {
  Injectable,
} from '@nestjs/common'

import { PostQueryDto } from './dto/post-query.dto'
import { PrismaService } from '../prisma/prisma.service'


@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {
    
  }

  async findAll(query: PostQueryDto) {
    const { page, limit } = query;
    // 分页的游标
    const skip = (((page || 1) - 1) * (limit || 10));
    const [total, posts] = await Promise.all([
      this.prisma.post.count(),
      this.prisma.post.findMany({ 
        skip,   // 跳过几个
        take: limit,  // 拿多少个
        orderBy: { id: 'desc' },  // 按id 降序
        include: {   // 关系型的数据
          user: {
            select: {   // 只要哪些记录
              id: true,
              name: true,
              avatars: {
                take: 1,
                select: {
                  filename: true,
                }
              }
            }
          },
          tags: {
            select: {
              tag: {
                select: {
                  name: true
                }
              }
            }
          },
          _count: {
            select: {
              likes: true,
              comments: true,
            }
          },
          files: {
            where: {
              mimetype: { startsWith: "image/" },
            },
            select: { filename: true }
          }
        }
      })
    ])

    // 查询数据,再整备一下
    const data = posts.map(post => ({
      id: post.id,
      title: post.title,
      // 将content 进行截取
      brief: post.content?post.content.substring(0, 100):'',
      // publishedAt: post.createdAt || null,
      user: {
        id: post.user?.id,
        name: post.user?.name,
        avatar: `http://localhost:3000/uploads/avatar/resized/${post.user?.avatars[0].filename}-small.jpg`
      },
      tags: post.tags.map(tag => tag.tag.name),
      totallikes: post._count.likes,
      totalcomments: post._count.comments,
      thumbnail: `http://localhost:3000/uploads/resized/${post.files[0].filename}-thumbnail.jpg` || "",
    }))
    // const total = await this.prisma.post.count();
    // console.log(total, "**************");
    return {
      items: data,
      total: total
    }
  }

  async createPost(data: {
    title: string;
    content: string;
    userId: string;
  }) {
    return this.prisma.post.create({
      data: {
        title: data.title,
        content: data.content,
        userId: Number(data.userId),
      }
    })
  }
}

findAll 使用 Promise.all 并发 count 和 findMany,提高性能。include 关联查询用户、标签、计数和文件。数据处理中生成 brief、avatar URL 等。

前端与后端协作:token 携带与刷新

前端在 axios 拦截器中添加 Authorization: Bearer access_token。过期时,捕获 401,用 refresh_token POST /auth/refresh 获新对,更新存储,重试请求。

后端刷新已在 AuthService refreshToken 中实现(前文假设),生成新 token 对。

总结

通过 NestJS 的 JWT Guard 和 Strategy,我们实现了对 API 如新增文章的鉴权保护。从 Strategy 定义到 Guard 应用,再到服务数据处理,每步都体现了框架的优雅。代码虽有小问题(如 URL 硬码),但易修复。欢迎讨论你的鉴权经验!