NestJS 入门(3):Guard 如何挡住未登录请求?

上一篇:NestJS 入门(2):依赖注入到底解决了什么问题? 讲完了「对象怎么接到一起」。

接下来更贴近业务的问题是:

有些接口谁都能调,有些接口必须先登录。Nest 把这道门放在哪里?

答案是 Guard(守卫)

实战里最常见的门,是 JWT + Passport 这一套。

本文会先讲清 Guard 在链路中的位置,再介绍相关插件各自干什么,最后用一个最小可跑例子把「登录发 token → 带 token 访问受保护接口」串起来。


1. Guard 在请求链路里的位置

一个请求进 Nest 后,大致顺序是:

text 复制代码
请求进来
  → 中间件(Middleware)
  → Guard(能不能进?)
  → 管道(Pipe,参数校验/转换)
  → Controller 方法
  → Service 业务逻辑

Guard 回答的是一个很单纯的问题:

这个请求,允不允许继续往下走?

允许 → 进入 Controller

不允许 → 直接返回 401 / 403,业务代码根本不会执行

这和 Service 里的业务判断不一样:

位置 典型问题
Guard 你是谁?有没有登录?有没有权限?
Service 密码对不对?这个项目属不属于你?数据怎么改?

鉴权门槛尽量放在 Guard;业务规则放在 Service。


2. JWT 相关插件:各自干什么?

Nest 官方没有「只装一个包就搞定登录」。常见组合是下面几个,职责要分清:

bash 复制代码
npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm i -D @types/passport-jwt
角色 你平时会碰到它的地方
@nestjs/jwt Nest 封装的 JWT 工具 JwtModule.register(...)、注入 JwtServicesign / verify
@nestjs/passport 把 Passport 接到 Nest PassportModuleAuthGuard('jwt')PassportStrategy
passport Node 生态里的认证框架 一般不直接写很多代码,但是底层依赖
passport-jwt Passport 的 JWT 策略实现 StrategyExtractJwt.fromAuthHeaderAsBearerToken()
@types/passport-jwt TypeScript 类型 开发期类型提示

可以记成一句话:

@nestjs/jwt 负责签发/校验 token;passport + passport-jwt 负责「从请求里取出 token 并按策略认证」;@nestjs/passport 把这套策略接到 Nest 的 Guard 上。

和登录常一起出现、但不属于 JWT 本体的:

作用
bcrypt 密码哈希与比对(存库用哈希,登录时 compare

它们在 Module 里通常这样装配:

typescript 复制代码
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'your-secret-key',
      signOptions: { expiresIn: '30m' },
    }),
  ],
  // controllers / providers ...
})
export class AuthModule {}
  • PassportModule:启用 Passport 集成
  • JwtModule.register({ secret, signOptions }):配置签名密钥和默认过期时间,并提供可注入的 JwtService

3. 最直观的用法:@UseGuards

登录接口本身不需要登录;「我是谁」接口必须登录:

typescript 复制代码
@Controller('api/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  login(@Body() body: { email: string; password: string }) {
    return this.authService.login(body.email, body.password);
  }

  @UseGuards(JwtAuthGuard)
  @Get('me')
  me(@Headers('authorization') auth: string) {
    const token = auth?.replace('Bearer ', '');
    return this.authService.validateToken(token);
  }
}

对比一下:

  • POST /api/auth/login:没有 Guard,任何人都能尝试登录
  • GET /api/auth/me:挂了 JwtAuthGuard,没带有效 token 进不来

项目列表这类业务接口也一样:

typescript 复制代码
@UseGuards(JwtAuthGuard)
@Get()
findAll(@Request() req: AuthenticatedRequest) {
  const userId = req.user?.userId;
  return this.projectsService.findAll(userId);
}

先过 Guard,再拿当前用户去查「属于我的项目」。


4. 方法级 vs 控制器级

Guard 可以挂在单个方法 上,也可以挂在整个 Controller 上。

方法级(上面那些例子):只保护某一个接口。

控制器级:这个 Controller 里所有接口默认都要登录:

typescript 复制代码
@Controller()
@UseGuards(JwtAuthGuard)
export class TaskPromptsController {
  constructor(private readonly service: TaskPromptsService) {}

  @Get('api/projects/:projectId/task-prompts')
  list(@Param('projectId') projectId: string) {
    return this.service.listByProject(projectId);
  }

  @Put('api/projects/:projectId/task-prompts/:templateKey')
  saveDraft(/* ... */) {
    // ...
  }
}

怎么选:

  • 只有少数接口要登录 → 方法级更清晰
  • 整个模块几乎都要登录 → 控制器级少写很多重复装饰器

5. JwtAuthGuard 为什么看起来这么短?

真实代码里,Guard 本身可能只有一行:

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

这里用的是 @nestjs/passport 提供的 AuthGuard

'jwt' 这个名字,对应的是 passport-jwt 策略。

真正的校验逻辑在 Strategy 里:

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email, name: payload.name };
  }
}

对照插件:

代码 来自哪个包
AuthGuard('jwt') @nestjs/passport
PassportStrategy(...) @nestjs/passport
Strategy / ExtractJwt passport-jwt

职责拆开:

角色 干什么
JwtAuthGuard 声明:这里用名为 jwt 的认证策略守门
JwtStrategy 从 Header 取 Bearer token、验签、解析 payload
validate() 验签通过后,把用户信息整理出来,挂到请求上

AuthGuard('jwt') 里的 'jwt',必须和策略名对得上。

Nest + passport-jwt 的默认约定里,策略名就是 jwt

另外:secretOrKey 要和 JwtModule.register({ secret })、登录时 JwtService.sign 用的密钥一致,否则「能签发、验不过」。


6. 通过 Guard 之后,用户信息在哪里?

Strategy 的 validate() 返回值,会被放到 request.user 上。

所以受保护接口可以这样取当前用户:

typescript 复制代码
interface AuthenticatedRequest extends ExpressRequest {
  user?: { userId: string; email: string; name: string };
}

@UseGuards(JwtAuthGuard)
@Get('generation-preferences')
getGenerationPreferences(@Request() req: AuthenticatedRequest) {
  const userId = req.user?.userId;
  if (!userId) {
    throw new UnauthorizedException('Unauthorized');
  }
  return this.authService.getGenerationPreferences(userId);
}

链路可以记成:

text 复制代码
Authorization: Bearer <token>
  → JwtStrategy 取出并校验 token(passport-jwt)
  → validate(payload) 返回 { userId, email, name }
  → 写入 req.user
  → Controller 用 @Request() 读取

Guard 负责「放行」;Controller / Service 负责「用这个身份继续办事」。


7. 签发 token:JwtService 用在 Service 里

Guard / Strategy 管「验票」;登录成功后「出票」通常在 Service,注入 @nestjs/jwtJwtService

typescript 复制代码
@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  private async generateTokens(user: { id: string; email: string; name: string }) {
    const payload = { sub: user.id, email: user.email, name: user.name };

    const accessToken = this.jwtService.sign(payload, {
      expiresIn: '30m',
    });

    const refreshToken = this.jwtService.sign(payload, {
      expiresIn: '7d',
    });

    return { accessToken, refreshToken };
  }
}

常见分工:

动作 常用 API 出现位置
登录成功发 token jwtService.sign(payload) AuthService
手动校验 token jwtService.verify(token) 少数自定义逻辑
请求自动验票 Strategy + Guard 受保护接口入口

生产里 access token 短、refresh token 长,是很常见的组合;细节可以后续再展开,入门先抓住「谁签发、谁校验」。


8. 还要在 Module 里注册

和上一篇 DI 一样:Guard、Strategy 也是 Provider,要登记。

typescript 复制代码
@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'your-secret-key',
      signOptions: { expiresIn: '30m' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtAuthGuard],
  exports: [AuthService, JwtAuthGuard],
})
export class AuthModule {}

实用提醒:

  1. JwtStrategy 必须进 providers,否则 Guard 找不到策略
  2. 其他模块要用 @UseGuards(JwtAuthGuard),通常需要 Auth 模块 exports 这个 Guard
  3. JwtModulesecretJwtStrategysecretOrKey 必须一致
  4. @nestjs/jwt@nestjs/passportpassport-jwt 缺一个,链路就接不上

9. 可选登录:OptionalJwtAuthGuard

有些接口「登录了更好,不登录也能看」。这时可以做可选 Guard:

typescript 复制代码
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(err: any, user: any) {
    return user;
  }
}

和强制 Guard 的差别在于:校验失败时,不直接把请求打死,而是可能让 user 为空,继续往下走。

业务代码再判断:有 req.user 就按登录态处理,没有就按游客处理。

入门阶段先掌握强制登录的 JwtAuthGuard 即可。


10. 对照一张完整链路图

text 复制代码
1) POST /api/auth/login
     → AuthService 校验账号密码(可用 bcrypt)
     → JwtService.sign(...) 签发 accessToken / refreshToken

2) GET /api/projects
     Header: Authorization: Bearer <accessToken>
     → JwtAuthGuard(@nestjs/passport)
     → JwtStrategy(passport-jwt)验签
     → validate() 写入 req.user
     → Controller / Service 使用 userId

如果第 2 步没有 token、token 过期、或签名不对:

  • Guard / Strategy 拦下请求
  • Controller 根本不会执行

11. 实战:最小可跑的 JWT 登录 + 受保护接口

下面用最少文件,搭一条能跑通的链路。目标只有两个接口:

  • POST /auth/login:账号密码正确 → 返回 token
  • GET /auth/profile:必须带 Authorization: Bearer <token>

11.1 安装依赖

bash 复制代码
npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm i -D @types/passport-jwt

11.2 auth.module.ts:装配插件

typescript 复制代码
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { JwtAuthGuard } from './jwt-auth.guard';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'dev-secret', // 实战请改成环境变量
      signOptions: { expiresIn: '30m' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtAuthGuard],
})
export class AuthModule {}

记得在 AppModuleimports: [AuthModule]

11.3 auth.service.ts:校验账号并签发 token

typescript 复制代码
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  // 演示用内存用户;真实项目换成数据库
  private readonly users = [
    { id: '1', email: 'demo@example.com', password: 'password123', name: 'Demo' },
  ];

  constructor(private readonly jwtService: JwtService) {}

  login(email: string, password: string) {
    const user = this.users.find((u) => u.email === email);
    if (!user || user.password !== password) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload = { sub: user.id, email: user.email, name: user.name };
    const accessToken = this.jwtService.sign(payload);

    return {
      accessToken,
      user: { id: user.id, email: user.email, name: user.name },
    };
  }
}

说明:这里为了最短路径用了明文密码。真实项目请用 bcrypt.hash / bcrypt.compare

11.4 jwt.strategy.ts:从 Header 取票并验票

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'dev-secret', // 必须和 JwtModule.register 的 secret 一致
    });
  }

  validate(payload: { sub: string; email: string; name: string }) {
    return { userId: payload.sub, email: payload.email, name: payload.name };
  }
}

11.5 jwt-auth.guard.ts:一道门

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

11.6 auth.controller.ts:公开登录 + 受保护资料

typescript 复制代码
import { Body, Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  login(@Body() body: { email: string; password: string }) {
    return this.authService.login(body.email, body.password);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  profile(@Request() req: { user: { userId: string; email: string; name: string } }) {
    return req.user;
  }
}

11.7 用 curl 验证

登录:

bash 复制代码
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"demo@example.com\",\"password\":\"password123\"}"

把返回里的 accessToken 拷出来,再访问受保护接口:

bash 复制代码
curl http://localhost:3000/auth/profile \
  -H "Authorization: Bearer 粘贴你的accessToken"

预期:

  • 带正确 token → 返回 { userId, email, name }
  • 不带 token / token 错误 → 401,进不了 profile 方法

如果这一步跑通了,你就已经同时练到了:

  1. @nestjs/jwt 签发
  2. passport-jwt + Strategy 校验
  3. @nestjs/passport 的 Guard 守门
  4. Controller 从 req.user 取当前用户

12. 小结

  • Guard 决定请求能不能进入 Controller
  • JWT 常见插件分工:@nestjs/jwt 出票/验票工具,passport-jwt 策略实现,@nestjs/passport 接到 Nest Guard
  • @UseGuards(JwtAuthGuard) 可挂在方法或整个 Controller
  • JwtModule.secretJwtStrategy.secretOrKey 必须一致
  • 登录用 JwtService.sign;访问受保护接口靠 Strategy + Guard 自动验票并写入 req.user

对照前两篇,请求相关的问题可以再多一句:

  1. 哪个 Controller 接请求?
  2. 哪个 Service 做业务?
  3. 哪个 Module 组装它们?
  4. 依赖从哪里注入?
  5. 这个接口有没有 Guard?JWT 相关插件在这条链路上各负责哪一段?

下一篇可以讲:统一响应与异常处理 ------为什么业务代码抛 UnauthorizedException,前端却总能收到结构一致的错误包。

系列导航

相关推荐
带娃的IT创业者1 小时前
DeepTutor:当 Agent-Native 架构撞上个性化学习的临界点
学习·架构·ai agent·大模型应用·个性化学习·教育技术·agent-native架构
kyriewen1 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
xian_wwq2 小时前
【学习笔记】Context Engineering,AI Agent 真正的内存管理-4/16
笔记·学习·context
IT_陈寒2 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635072 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
用户938515635072 小时前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端
寒月小酒2 小时前
AnythingLLM 学习
学习
油丶酸萝卜别吃3 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang3 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试