上一篇: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(...)、注入 JwtService 去 sign / verify |
@nestjs/passport |
把 Passport 接到 Nest | PassportModule、AuthGuard('jwt')、PassportStrategy |
passport |
Node 生态里的认证框架 | 一般不直接写很多代码,但是底层依赖 |
passport-jwt |
Passport 的 JWT 策略实现 | Strategy、ExtractJwt.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/jwt 的 JwtService:
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 {}
实用提醒:
JwtStrategy必须进providers,否则 Guard 找不到策略- 其他模块要用
@UseGuards(JwtAuthGuard),通常需要 Auth 模块exports这个 Guard JwtModule的secret与JwtStrategy的secretOrKey必须一致@nestjs/jwt、@nestjs/passport、passport-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:账号密码正确 → 返回 tokenGET /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 {}
记得在 AppModule 里 imports: [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方法
如果这一步跑通了,你就已经同时练到了:
@nestjs/jwt签发passport-jwt+ Strategy 校验@nestjs/passport的 Guard 守门- Controller 从
req.user取当前用户
12. 小结
- Guard 决定请求能不能进入 Controller
- JWT 常见插件分工:
@nestjs/jwt出票/验票工具,passport-jwt策略实现,@nestjs/passport接到 Nest Guard @UseGuards(JwtAuthGuard)可挂在方法或整个 ControllerJwtModule.secret与JwtStrategy.secretOrKey必须一致- 登录用
JwtService.sign;访问受保护接口靠 Strategy + Guard 自动验票并写入req.user
对照前两篇,请求相关的问题可以再多一句:
- 哪个 Controller 接请求?
- 哪个 Service 做业务?
- 哪个 Module 组装它们?
- 依赖从哪里注入?
- 这个接口有没有 Guard?JWT 相关插件在这条链路上各负责哪一段?
下一篇可以讲:统一响应与异常处理 ------为什么业务代码抛 UnauthorizedException,前端却总能收到结构一致的错误包。