프레임워크/Nest.js

Nest.js 의 가드(Guard) 란?

Jake Seo 2024. 2. 2. 22:23

Guard (가드) 란?

개념

  • Nest.js 에서 요청이 실제로 처리되기 전에 보통 권한이 있는지 확인하는 방패막이(Guard) 역할을 해주는 컴포넌트다.
  • 가드가 없었다면 그냥 컨트롤러로 전해질수도 있는 요청에 이 가드가 중간 브로커 역할을 해서 인증 정보를 확인하거나 사용자 정보를 넣어주는 역할을 하는 것이다.

picture 2

Nest.js 의 라이프사이클로 보는 가드(Guard)의 위치

세부 설명

  • CanActive 인터페이스를 구현하고, @Injectable 데코레이터가 붙은 클래스이다.
  • 런타임에 존재하는 조건에 따라 요청이 라우트 핸들러에 의해 처리될지 여부를 결정한다.
    • 조건이란, 권한, 역할(ROLE), ACLs(Access Control Lists) 등을 말한다.
  • 흔히 Authorization 이라 불리는 것들과 연관이 깊다.
  • Express 애플리케이션에서는 미들웨어가 이 역할을 한다.
    • 미들웨어로도 처리가 가능하지만, Guard 는 ExecutionContext 인스턴스에 접근이 가능해서 다음에 무엇이 실행될지 알 수 있는 것이 차이다.

picture 0

예제 인증 가드 구현

BasicTokenGuard

  • Basic Token 을 통해 인증을 진행하는 가드의 구현 예제이다.
  • 마지막에 request.user 에 사용자 정보를 넣어주는데 @Controller 메서드에서 이 가드를 거치면, @Request 데코레이터를 통해 해당 사용자 객체에 접근할 수 있다.
@Injectable()
export class BasicTokenGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();

    const rawToken = req.headers["authorization"];

    if (!rawToken) {
      throw new UnauthorizedException("인증 토큰이 존재하지 않습니다.");
    }

    const token = this.authService.extractTokenFromHeader(rawToken, false);

    const { email, password } = this.authService.decodeBasicToken(token);

    const user = this.authService.authenticateWithEmailAndPassword({
      email,
      password,
    });

    req.user = user;

    return true;
  }
}

BearerTokenGuard

  • BearerTokenGuard 는 사실상 상속용으로 구현했고, 실제로 사용되는 건 RefreshTokenGuard 이다.
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";
import { AuthService, JwtPayload } from "../auth.service";
import { UsersService } from "src/users/users.service";
import { UsersModel } from "src/users/entities/users.entity";

interface BearerGuardRequest {
  token: string;
  tokenType: JwtPayload["type"];
  user: UsersModel;
  headers: {
    authorization: string;
  };
}

@Injectable()
export class BearerTokenGaurd implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly usersService: UsersService
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    const rawToken = req.headers["authorization"];

    if (!rawToken) {
      throw new UnauthorizedException("인증 토큰이 존재하지 않습니다.");
    }

    const token = this.authService.extractTokenFromHeader(rawToken, true);

    const payload = this.authService.verifyToken(token);

    req.token = token;
    req.tokenType = payload.type;
    req.user = await this.usersService.getUserByEmail(payload.email);

    return true;
  }
}

@Injectable()
export class AccessTokenGuard extends BearerTokenGaurd {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    await super.canActivate(context);

    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    if (req.tokenType !== "access") {
      throw new UnauthorizedException("액세스 토큰이 아닙니다!");
    }

    return true;
  }
}

@Injectable()
export class RefreshTokenGuard extends BearerTokenGaurd {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    await super.canActivate(context);

    const req: BearerGuardRequest = context.switchToHttp().getRequest();

    if (req.tokenType !== "refresh") {
      throw new UnauthorizedException("리프레시 토큰이 아닙니다!");
    }

    return true;
  }
}
반응형