Skip to content

守卫

守卫,守护保卫在路由方法之前的一道关卡。

一个被 @Injectable 注解装饰的类,实现了 CanActivate 就能被称为守卫。

守卫最多的应用场景,就是用来校验访问者的权限,如果不具有某个权限,可以将当前请求拒绝掉。

守卫的设计基于 AOP 思想实现,所以非常类似 异常过滤器管道拦截器,它和中间件有些定义上的差异。

ExpressKoaNodeJS 后端框架中,通常由中间件来完成守卫的工作。中间件来验证身份是不错的选择,但中间件来做这件事情本质上是愚蠢的,因为中间件不知道调用的 next() 函数后执行哪个函数方法。而守卫存在 ExecutionContext 参数,可以确切知道下一步要执行的内容。

熟悉前端开发的同学来说,守卫听起来与 Vue 的前置路由守卫很像。守卫可以类比前置路由守卫,在 Vue 中能做的事,那么守卫也能做

使用守卫

正如前面所述,授权对于守卫来说是一个很好的应用场景,所以我们这里定义一个简单逻辑的守卫验证,它将会验证请求是否包含 token 请求头来决定请求是否继续:

ts
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest<Request>();

    const authorization = request.header("Authorization");

    return !!authorization;
  }
}

每个守卫必须实现接口并完善 canActivate 函数,该函数应该返回一个布尔值,来决定是否允许请求。

  • true:请求将被处理。
  • false:Nest 将会拒绝该请求,并抛出错误。

接下来将守卫注册使用,绑定在控制器上:

ts
// app.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import { AppService } from "./app.service";
import { AuthGuard } from "./guards/auth.guard";

@UseGuards(AuthGuard)
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

当用户请求接口时,如果未传递请求头 Authorization 字段,将会返回错误信息给用户:

image-20240123234123751

当满足预期传递了指定请求头时,守卫将会允许该请求继续并处理后续程序:

image-20240123234229643

绑定位置

守卫与其他的组件一样,能在局部、全局进行绑定。

局部:

  • 控制器
  • 路由方法
ts
// 1.绑定控制器
@UseGuards(AuthGuard)
@Controller()
export class AppController {

  // 2.绑定路由方法
  @UseGuards(AuthGuard)
  @Get()
  getHello(): string {}
  
  // ...
}

全局:

  • 主入口全局绑定,使用 app.useGlobalGuards
  • 根模块依赖注入
ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 1.主入口全局绑定
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
bootstrap();

// ---------------------------------------------

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    // 2.根模块依赖注入绑定
    AppService,
    {
      provide: APP_GUARD,
      useClass: AuthGuard
    }
  ]
})
export class AppModule {}

JWT

JWT(JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。

TIP

关于 Token 的更多知识,请点击这里学习。

Token 最常见的场景就是发放授权,用户在请求登录时,服务端会发放 Token 作为用户的凭证。后续用户的每个请求都会携带该凭证,服务端在处理用户请求时,优先安全校验凭证是否存在、合法,来决定该请求是否继续处理。

实现一个发放用户 Token 的简单案例,该案例会模拟发放 Token 、过滤异常信息,实际生产会比这复杂好几个维度。

Nest 集成了 JWT ,使其成为了一个内置模块,我们仅需要安装并从依赖中引入。

shell
pnpm i @nestjs/jwt -S

接着定义一组模拟数据,方便后续测试:

点击查看代码 src/constants/user.mock.ts
ts
export const users = [
  {
    id: 1,
    name: "王洋",
    password:"wy123456"
  },
  {
    id: 2,
    name: "李秀英",
    password:"lxy123456"
  },
  {
    id: 3,
    name: "杨敏",
    password:"ym123456"
  },
  {
    id: 4,
    name: "吴静",
    password:"wj123456"
  },
  {
    id: 5,
    name: "黄敏",
    password:"hm123456"
  }
];

把获取用户数据的操作存放在 User 模块中,把进行权限校验的操作放在 Auth 中,这会让我们更加省事,使用脚手架命令生成:

shell
nest g res user auth --no-spec

定义登录、获取用户接口提供用户访问。

ts
// 抛出定义的秘钥
export const TOKEN_SECRET = "guard example test token secret";
ts
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { UserService } from "../user/user.service";
import { JwtModule } from "@nestjs/jwt";
import { TOKEN_SECRET } from "../constants/config";
import { UserModule } from "../user/user.module";

@Module({
  imports: [
    // jwtModule动态注册
    // 注册在 authModule 是为了不用在providers中引入
    JwtModule.registerAsync({
      useFactory: () => ({
        secret: TOKEN_SECRET,
        global: true,
        signOptions: { expiresIn: "1d" }
      })
    }),
    UserModule
  ],
  controllers: [AuthController],
  providers: [AuthService, UserService]
})
export class AuthModule {}
ts
// auth.controller.ts 权限模块控制器
import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";

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

  @Post("login")
  login(@Body() body: Record<string, any>) {
    return this.authService.signIn(body.name, body.password);
  }
}
ts
// auth.service.ts 权限服务
import { ForbiddenException, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { UserService } from "../user/user.service";
import { AUTH_PWD_OR_ACCOUNT_ERROR } from "../filters/error-code";

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService
  ) {}

  /**
   * 对用户信息做验证,合法发放token,非法抛出异常4030000003
   * @param name
   * @param pwd
   */
  async signIn(name: string, pwd: string) {
    const user = await this.userService.findOneByName(name);

    if (!user || user.name !== name || user.password !== pwd) {
      throw new ForbiddenException({
        errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
        message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
        code: AUTH_PWD_OR_ACCOUNT_ERROR.code
      });
    }

    // 发放token
    const payload = { sub: user.id, username: user.name };
    return {
      access_token: await this.jwtService.signAsync(payload)
    };
  }
}
ts
// user.service.ts 用户服务
import { Injectable } from "@nestjs/common";
import { users } from "../constants/user.mock";

@Injectable()
export class UserService {
  findOne(id: number) {
    return users.find((item) => item.id === id);
  }

  findOneByName(name: string) {
    return users.find((item) => item.name === name);
  }
}

HttpExceptionFilterAUTH_PWD_OR_ACCOUNT_ERROR 在异常过滤器定义过。

image-20240124211206380

可以看到,发送正确的请求,会响应 Token 字符串给回客户端。

TIP

在使用 JWT 用到动态模块注册,在后续的篇章会介绍如何使用。请点击这里学习

凭证校验

上面已经学习过守卫的作用,也学习了如何发放 Token 。接下来将它们结合在一起,完成案例,当用户在登录时,如未携带凭证,服务端拒绝掉此次请求。

定义一个接口,获取用户的个人信息(通常如不是开放的接口,均需要凭证验证):

ts
import { Controller, Get, Param, Req, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { users } from "../constants/user.mock";
import { Request } from "express";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get("self")
  findOne(@Req() req: Request & Record<any, any>) {
    const { user } = req;
    return user;
  }
}

在请求获取用户信息接口时,携带我们刚才获得的 access_token

我们可以实现最后一个需求,实现一个认证守卫。

校验用户是否携带 Token ,是否合法,定义一个 auth 守卫:

ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Observable } from "rxjs";
import { Request } from "express";
import { AUTH_PWD_OR_ACCOUNT_ERROR } from "../filters/error-code";
import { JwtService } from "@nestjs/jwt";
import { TOKEN_SECRET } from "../constants/config";

@Injectable()
export class AuthGuard1 implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new ForbiddenException({
        errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
        message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
        code: AUTH_PWD_OR_ACCOUNT_ERROR.code
      });
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: TOKEN_SECRET
      });
      // 在这里我们将 payload 挂载到请求对象上
      // 以便我们可以在路由处理器中访问它
      request["user"] = payload;
    } catch {
      throw new ForbiddenException({
        errorCode: AUTH_PWD_OR_ACCOUNT_ERROR.errorCode,
        message: AUTH_PWD_OR_ACCOUNT_ERROR.message,
        code: AUTH_PWD_OR_ACCOUNT_ERROR.code
      });
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(" ") ?? [];
    return type === "Bearer" ? token : undefined;
  }
}

把守卫注册在需要验证的接口,比如不开放的获取用户信息的接口:

ts
import { Controller, Get, Param, Req, UseGuards } from "@nestjs/common";
import { UserService } from "./user.service";
import { users } from "../constants/user.mock";
import { Request } from "express";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @UseGuards(AuthGuard) 
  @Get("self")
  findOne(@Req() req: Request & Record<any, any>) {
    const { user } = req;
    return user;
  }
}

发送一个携带错误 access_token 的请求,结果如下:

image-20240124213323205

接着我们发送一个正确的,结果如预期一样,返回用户的信息数据:

image-20240124213400901

TIP

关于更多的守卫授权的案例学习,请点击这里

案例一:身份验证守卫

案例二:授权守卫

如有转载或 CV 的请标注本站原文地址