CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/Guard 与认证
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

Guard 与认证

@UseGuards、JWT 概念、与 Spring Security 对照。

35 分钟

Guard 与认证

这一课你将能做到

  1. 实现登录接口签发 JWT,并写出可校验 Bearer Token 的 JwtAuthGuard
  2. 用自定义 @Roles() 装饰器配合 RolesGuard 做角色控制(仅管理员能增删题目)
  3. 区分 401 与 403,说出全局 Guard 加 @Public() 的放行模式

为什么要学

Guard(守卫)在请求进入 Controller 之前回答一个问题:「这个请求有资格继续吗?」判定包括是否登录、令牌是否有效、角色是否足够;不通过时框架直接返回 401/403,业务方法不会执行。

刷题平台规则:浏览做题人人可访问,增删改仅管理员,提交作答必须登录。这些判断散落在 Service 中必然重复易漏,Guard 把鉴权收敛成前置层,对应 Spring Security 过滤器链加 @PreAuthorize。

先建立图像

请求 → Guard(认证/鉴权)──失败→ 401(未认证)/ 403(无权限)
     → Pipe(校验转换)→ Controller → Service

JWT:login 拿 access_token;请求带 Bearer 头,
Guard 验签验过期并把用户挂到 request.user。
NestJSSpring Security
CanActivate请求过滤器 / 鉴权决策器
@UseGuards(...)@PreAuthorize 方法级安全
request.userSecurityContextHolder 中的 Principal
全局 Guard + @Public()默认拦截 + permitAll() 白名单

跟着做

步骤 1:安装 JWT 模块并创建 AuthModule

npm install @nestjs/jwt
nest g module auth && nest g controller auth --no-spec && nest g service auth --no-spec

步骤 2:实现登录签发 Token

src/auth/auth.service.ts:

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

export type Role = "student" | "admin";

interface UserRow {
  id: number; username: string; password: string; role: Role;
}

@Injectable()
export class AuthService {
  // 接库后换真实查询,密码存哈希
  private users: UserRow[] = [
    { id: 1, username: "student1", password: "123456", role: "student" },
    { id: 2, username: "admin", password: "admin123", role: "admin" },
  ];

  constructor(private readonly jwt: JwtService) {}

  login(username: string, password: string) {
    const user = this.users.find((u) => u.username === username && u.password === password);
    if (!user) {
      throw new UnauthorizedException("用户名或密码错误");
    }
    const payload = { sub: user.id, username: user.username, role: user.role };
    return { access_token: this.jwt.sign(payload) };
  }
}

src/auth/auth.controller.ts:

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

  @Post("login")
  login(@Body() dto: { username: string; password: string }) {
    return this.auth.login(dto.username, dto.password);
  }
}

src/auth/auth.module.ts:

@Module({
  imports: [
    JwtModule.register({ secret: "dev-secret-change-me", signOptions: { expiresIn: "2h" } }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
  exports: [JwtModule], // 供其他模块 Guard 使用
})
export class AuthModule {}

登录验证:POST /api/auth/login,body 为 {"username":"admin","password":"admin123"},应返回 { access_token }。

步骤 3:实现 JwtAuthGuard

新建 src/auth/guards/jwt-auth.guard.ts:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

export interface RequestUser {
  id: number; username: string; role: "student" | "admin";
}

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwt: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    const header: string | undefined = req.headers.authorization;
    if (!header?.startsWith("Bearer ")) {
      throw new UnauthorizedException("缺少 Bearer Token");
    }
    try {
      const payload = this.jwt.verify<{
        sub: number; username: string; role: RequestUser["role"];
      }>(header.slice(7));
      req.user = { id: payload.sub, username: payload.username, role: payload.role };
      return true;
    } catch {
      throw new UnauthorizedException("Token 无效或已过期");
    }
  }
}

步骤 4:用 @CurrentUser 取登录用户

新建 src/auth/decorators/current-user.decorator.ts:

export const CurrentUser = createParamDecorator(
  (_data: unknown, context: ExecutionContext): RequestUser =>
    context.switchToHttp().getRequest().user,
);

改造 SubmissionsController 的提交方法(用登录用户 id 替换写死的 1001):

@Post()
@UseGuards(JwtAuthGuard)
submit(@Body() dto: SubmitAnswerDto, @CurrentUser() user: RequestUser) {
  return this.submissions.submit(dto.questionId, user.id, dto.selectedIndex);
}

带 Token 提交 id=1、selectedIndex=0 返回 correct: true;无头返回 401。

步骤 5:角色控制——@Roles 加 RolesGuard

新建 src/auth/decorators/roles.decorator.ts:

export const ROLES_KEY = "roles";
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

新建 src/auth/guards/roles.guard.ts:

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!required?.length) {
      return true; // 未声明角色要求,放行
    }
    const user = context.switchToHttp().getRequest().user as
      | RequestUser | undefined;
    if (!user) {
      throw new UnauthorizedException();
    }
    if (!required.includes(user.role)) {
      throw new ForbiddenException("权限不足,需要:" + required.join(","));
    }
    return true;
  }
}

在 QuestionsController 的写操作上挂载:

@Delete(":id")
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles("admin")
@HttpCode(204)
remove(@Param("id", ParseIntPipe) id: number) {
  this.questions.remove(id);
}

student1 的 Token 调创建接口得 403;admin 的 Token 得 201。

步骤 6:全局 Guard 加 @Public 放行(推荐模式)

逐个挂容易漏:默认全部接口要登录,再标记公开接口。

新建 src/auth/decorators/public.decorator.ts:

export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

JwtAuthGuard 注入 Reflector,canActivate 开头加:

if (this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
  context.getHandler(), context.getClass(),
]))
  return true;

AppModule 的 providers 中全局注册两个 Guard:

{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },

最后给登录和公开查询接口贴 @Public(),对应 Spring Security 的 permitAll 白名单。

讲清楚要点

  1. 决策前置:Guard 在 Pipe 之前执行,未登录请求不消耗业务资源。
  2. 401 与 403 语义不同:没登录或令牌无效是 401;登录了但角色不够是 403。
  3. 认证与授权分层:JwtAuthGuard 解决「你是谁」,RolesGuard 解决「你能做什么」。
  4. Reflector 读元数据:装饰器写、Guard 读,是 Nest 扩展点的通用模式。
  5. 全局默认拒绝更安全:新接口默认受保护,忘挂守卫也不会裸奔。
  6. 密码红线:生产必须存 bcrypt/argon2 哈希,密钥放环境变量。

你可能会懵的地方

  • Guard 里能注入 Service 吗? 能。Guard 是 @Injectable,可查库做 token 黑名单,在对应模块登记即可。

  • JWT 存哪里?前端怎么带? 存浏览器内存或 httpOnly Cookie;请求放 Authorization: Bearer <token> 头。payload 只签名不加密,不放敏感信息。

  • @UseGuards 传类和传实例的区别? 传类由容器解析、可注入依赖;new MyGuard() 的实例无法注入。初学者记住一律传类。

  • 多个 Guard 的执行顺序? 按 @UseGuards(A, B) 声明顺序执行,任一失败立即中断;全局 Guard 先于控制器级 Guard。

错法对照

错法问题正解
Service 里读 header 判断登录重复、易漏、难测试Guard 统一前置
JWT 密钥写死源码随仓库泄露ConfigModule + 环境变量
角色不够也返回 401语义混淆未认证 401、权限不足 403
把 body 格式校验写进 Guard职责混乱格式交 Pipe,权限交 Guard
全局拦截却忘给 login 加 @Public无法获取 Token登录接口显式放行

卡点急救

  1. 所有请求都 401:确认公开接口贴了 @Public();头格式 Bearer <token>(一个空格)。
  2. Token 解析失败:签发校验须同一 secret;检查 expiresIn 是否过期。
  3. RolesGuard 永远 403:核对 payload 与 @Roles("admin") 的 role 拼写一致。

面试常问

  • Guard 的执行时机与用途? 中间件之后、Pipe 之前执行,返回布尔值决定是否继续;失败映射 401/403。

  • 401 和 403 的区别? 401 是未认证或凭据无效,语义「先证明你是谁」;403 是已认证但不允许访问,语义「知道你是谁,但你不能做」。

  • NestJS 怎么做方法级权限控制? SetMetadata 写角色装饰器,注入 Reflector 的 Guard 读取所需角色与请求用户比对,经 @UseGuards 或 APP_GUARD 生效,对应 Spring 的 @PreAuthorize。

自测三问

  1. JWT Bearer 登录流程的三个步骤是什么?
  2. JwtAuthGuard 与 RolesGuard 分别回答什么问题?为什么拆成两个?
  3. 全局 Guard 模式下,如何让登录接口免登录访问?

小结一句话

Guard 是路由级鉴权前置层:JwtAuthGuard 管身份、RolesGuard 管权限,全局默认拒绝加 @Public 放行最稳妥。

延伸

  • Guards(官方守卫文档)
  • Authentication(认证实战,含 Passport)
  • 下一课预告:接入 ConfigModule,用环境变量管理 JWT 密钥与端口,让配置可注入、可校验。
加载本节练习…
← 上一课Pipe 与校验下一课 →ConfigModule