本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 实现
register与login两条认证流程。 - 登录后签发 JWT Token,并支持读取当前用户信息。
- 建立 BBS 的身份认证主链路。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
2.1 先理解 Guard:它在认证里到底做什么
Guard 是请求阶段的“入口判断器”。它运行在 Controller 方法执行前,决定这个请求是否允许继续往下走。
在认证场景里,Guard 最重要的职责不是自己写完整校验逻辑,而是:
- 拦截请求;
- 触发对应认证策略;
- 在校验成功后,把用户信息挂到请求对象上;
- 在校验失败时,直接拒绝请求。
所以你可以把 Guard 理解成认证链路的“开关”。
2.2 再理解 Strategy:它负责定义“怎么验证”
Strategy 负责描述验证规则,而不是决定是否拦截请求。
在本课里有两种 Strategy:
LocalStrategy:校验登录时提交的邮箱和密码;JwtStrategy:校验后续请求里携带的 JWT Token。
也就是说:
Guard负责“什么时候触发校验”;Strategy负责“具体怎么校验”。
2.3 为什么认证里要同时有 Guard 和 Strategy
这是 Nest + Passport 认证模型的核心拆分。
如果没有 Guard,每个 Controller 方法都要自己手动校验身份;
如果没有 Strategy,每种认证方式的实现就会散落在各个接口里,无法复用。
两者配合后,请求链路会变得很清晰:
- 请求先进入 Guard;
- Guard 触发对应 Strategy;
- Strategy 完成身份校验;
- 成功后把结果放到
req.user; - Controller 再读取
req.user继续处理业务。
2.4 本课认证链路的职责拆分
AuthController:定义路由,接收请求;AuthService:处理注册、密码比对、签发 Token;LocalAuthGuard/JwtAuthGuard:拦截请求并触发认证;LocalStrategy/JwtStrategy:定义具体验证规则;RolesGuard:在已登录基础上继续做角色权限判断。
2.5 Local 与 JWT 的分工
- Local:解决“你登录时是不是这个人”;
- JWT:解决“你后续请求时还能不能证明自己是谁”。
所以登录请求和业务请求的验证方式并不一样。
2.6 一次请求里,用户是如何被验证的
以两类请求为例:
-
POST /auth/login登录请求先经过LocalAuthGuard,再进入LocalStrategy。
LocalStrategy会调用AuthService.validateUser()去查用户、比对密码。成功后把用户信息写入req.user,Controller 再签发 JWT。 -
GET /auth/profile或带权限的业务接口 请求先经过JwtAuthGuard,再进入JwtStrategy。
JwtStrategy从请求头提取 Bearer Token,校验签名和过期时间,再把 token 中的用户信息恢复为req.user。如果接口还用了RolesGuard,会继续检查req.user.role是否满足要求。
2.7 JWT 的解析是如何实现的
在这个项目里,JWT 不是在 Controller 或 AuthService 里手动解析的,而是交给 JwtAuthGuard + JwtStrategy 这一套来完成。
执行过程可以拆成四步:
JwtAuthGuard拦截请求;- Passport 根据
AuthGuard('jwt')找到JwtStrategy; JwtStrategy从请求头里提取 Bearer Token,并完成签名与过期时间校验;- 校验通过后,
validate(payload)把 payload 转成统一的用户对象,挂到req.user。
所以你在 Controller 里看到的 req.user,并不是 Controller 自己生成的,而是 Guard 和 Strategy 在前面已经处理好的结果。
2.8 JwtModule.registerAsync 解决什么问题
JWT 密钥通常来自环境变量。registerAsync 允许你通过 ConfigService 动态读取配置,让开发、测试、生产环境使用不同密钥而不需要改代码。
2.9 如果希望“默认所有路由都需要登录”,应该怎么做
当前这节课的实现方式是“谁需要鉴权,就在谁的路由上显式挂 @UseGuards(JwtAuthGuard)”。
但在真实项目里,更常见的需求是:默认所有业务路由都需要登录,只有注册、登录这类接口例外。
这种场景的标准做法是:
- 把
JwtAuthGuard注册成全局 Guard; - 再提供一个
@Public()装饰器,用来标记不需要登录的白名单路由。
这样设计的好处是:
- 安全默认值更高,避免漏挂 Guard;
- 新增业务接口时,默认就受保护;
- 登录、注册、健康检查等公共接口可以显式放行。
2.10 canActivate() 到底是什么意思
canActivate() 可以直接理解成一句话:这个请求现在能不能继续往下执行。
它的返回值含义很明确:
- 返回
true:放行,请求继续进入后续链路; - 返回
false:拒绝,请求不会进入 Controller; - 抛出异常:立即中断,并返回对应错误响应;
- 返回
Promise<boolean>或Observable<boolean>:只是异步版本,语义不变。
所以当你看到 Guard 里实现 canActivate() 时,本质上就是在写“放行规则”。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
3.1 src/auth/auth.module.ts - 认证模块装配
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET', 'dev-secret'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
LocalAuthGuard,
JwtAuthGuard,
RolesGuard,
],
exports: [AuthService, JwtAuthGuard, RolesGuard],
})
export class AuthModule {}
3.2 src/auth/auth.controller.ts - 在路由上挂 Guard
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { AuthUser } from './types/auth-user';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@UseGuards(LocalAuthGuard)
@Post('login')
login(@Req() req: Request & { user: AuthUser }) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
profile(@Req() req: Request & { user: AuthUser }) {
return req.user;
}
}
这里最关键的一点是:
login() 和 profile() 进入方法体之前,用户身份已经由 Guard 完成验证。Controller 只消费 req.user。
3.3 src/auth/guards/local-auth.guard.ts - 登录入口守卫
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
这个类本身非常薄,因为真正的校验逻辑不写在 Guard 里,而是交给名为 local 的 Strategy。
3.4 src/auth/strategies/local.strategy.ts - 登录阶段校验
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { LoginDto } from '../dto/login.dto';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private readonly authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string) {
const dto: LoginDto = { email, password };
return this.authService.validateUser(dto);
}
}
3.5 src/auth/auth.service.ts - 注册、密码校验与签发 Token
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { UsersService } from '../users/users.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { AuthUser } from './types/auth-user';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async register(dto: RegisterDto): Promise<{
accessToken: string;
user: AuthUser;
}> {
const user = await this.usersService.create(dto);
return this.login({
userId: user.id,
email: user.email,
username: user.username,
role: user.role,
});
}
async validateUser(dto: LoginDto): Promise<AuthUser> {
const user = await this.usersService.findOneByEmail(dto.email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const matched = await bcrypt.compare(dto.password, user.passwordHash);
if (!matched) {
throw new UnauthorizedException('Invalid credentials');
}
return {
userId: user.id,
email: user.email,
username: user.username,
role: user.role,
};
}
async login(
user: AuthUser,
): Promise<{ accessToken: string; user: AuthUser }> {
const payload = {
sub: user.userId,
email: user.email,
username: user.username,
role: user.role,
};
return {
accessToken: await this.jwtService.signAsync(payload),
user,
};
}
}
3.6 src/auth/guards/jwt-auth.guard.ts 与 src/auth/strategies/jwt.strategy.ts - 持续访问校验
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthUser } from '../types/auth-user';
interface JwtPayload {
sub: number;
email: string;
username: string;
role: 'user' | 'moderator' | 'admin';
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET', 'dev-secret'),
});
}
validate(payload: JwtPayload): AuthUser {
return {
userId: payload.sub,
email: payload.email,
username: payload.username,
role: payload.role,
};
}
}
这一段代码正好对应 JWT 解析的四个动作:
ExtractJwt.fromAuthHeaderAsBearerToken():从Authorization: Bearer <token>中提取 token;secretOrKey:使用密钥校验签名;ignoreExpiration: false:要求 token 不能过期;validate(payload):把解码结果转换成项目内部统一使用的AuthUser,并交给后续请求链路使用。
3.7 src/auth/guards/roles.guard.ts - 登录之后再做权限判断
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import type { UserRole } from '../../users/users.repository';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context
.switchToHttp()
.getRequest<{ user?: { role?: UserRole } }>();
return !!request.user?.role && requiredRoles.includes(request.user.role);
}
}
这说明认证和鉴权是两层事情:
JwtAuthGuard先确认“你是谁”;RolesGuard再确认“你有没有权限做这件事”。
3.8 一次请求的真实执行顺序
以 PATCH /users/:id/role 为例:
- 请求先进入
JwtAuthGuard; JwtAuthGuard触发JwtStrategy校验 Token;- 校验成功后,用户信息被放到
req.user; RolesGuard继续检查req.user.role是否满足@Roles('admin');- 通过后,Controller 方法才开始执行;
@CurrentUser()只是把req.user取出来,并不负责验证。
3.9 进阶方案:把 JwtAuthGuard 变成全局 Guard
这不是当前分支已经落地的代码,而是你在真实项目中很常见的下一步演进方案。
第一步,定义 @Public() 装饰器:
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
第二步,让 JwtAuthGuard 支持跳过公共路由:
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
第三步,用 APP_GUARD 注册全局 Guard:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
JwtAuthGuard,
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
},
],
})
export class AuthModule {}
第四步,在不需要登录的接口上显式标记:
@Public()
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Public()
@Post('login')
login(@Req() req: Request & { user: AuthUser }) {
return this.authService.login(req.user);
}
这样之后,系统行为会变成:
- 默认所有路由都先经过
JwtAuthGuard; - 标记了
@Public()的路由直接放行; - 登录后的接口仍然可以继续叠加
RolesGuard做角色权限控制。
3.10 本节落地价值
你已经把“登录一次、后续带 Token 访问”的标准认证模型跑通,这是任何真实后端系统的核心基础。
4. 总结这节课知识点
- 你理解了
Guard在认证里的入口职责。 - 你掌握了
Guard -> Strategy -> req.user -> Controller这条认证主链路。 - 你理解了 Local/JWT Strategy 的职责边界。
- 你知道了认证和鉴权是两层不同的校验。
- 你知道了如何把
JwtAuthGuard演进为“默认全局鉴权 +@Public()白名单放行”模式。 - 你可以继续扩展 Refresh Token、黑名单、第三方登录等能力。
5. 每节课分支
lesson-05-auth-jwt- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git