在现代后端开发中,API 鉴权是保障资源安全的关键环节。NestJS 作为一款模块化且高效的框架,提供了强大的守卫(Guard)机制,结合 JWT(JSON Web Token) 可以轻松实现用户身份验证。例如,对于新增文章、点赞等需要权限的操作,用户必须先登录,并通过 token 携带身份信息。access_token 和 refresh_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 硬码),但易修复。欢迎讨论你的鉴权经验!