Node
Nest.js
Guard & Strategy

NestJS Guard와 Strategy 사용 방법

결론

  • Guard는 요청이 라우트 핸들러에 도달하기 전에 접근 권한을 검사하는 클래스입니다. CanActivate 인터페이스를 구현하며, ExecutionContext에 접근하여 실행될 핸들러 정보를 알 수 있습니다 [NestJS Guards 공식 문서] (opens in a new tab)

NestJS 공식 문서에서 "A guard is a class annotated with the @Injectable() decorator, which implements the CanActivate interface. Guards determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.)" 라고 명시되어 있습니다.

NestJS 공식 문서에서 "The PassportStrategy mixin calls passport.use(new Strategy(this.validate.bind(this))) under the hood, so that passport gets configured and is aware of the strategy and how to use it" 라고 설명합니다.

Stack Overflow에서 "AuthGuard() takes in a strategy name and returns a guard class, which is a mixin. This guard has its own canActivate method and calls passport.authenticate(strategy), handling the response from passport by either allowing the guard to return true or throwing UnauthorizedException to return a 401" 라고 설명되어 있습니다.

Guard란 무엇인가?

정의 및 역할

Guard는 NestJS의 요청-응답 주기에서 접근 제어를 담당하는 컴포넌트입니다. Middleware와 달리 ExecutionContext 인스턴스에 접근할 수 있어 다음에 실행될 핸들러를 정확히 알 수 있습니다.

NestJS 공식 문서에서 "Middleware is a fine choice for authentication, but middleware, by its nature, is dumb. It doesn't know which handler will be executed after calling the next() function. On the other hand, Guards have access to the ExecutionContext instance, and thus know exactly what's going to be executed next" 라고 명시되어 있습니다. [NestJS Guards 공식 문서] (opens in a new tab)

기본 구현

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
 
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Guard 적용 방법

컨트롤러 레벨:

@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {}

메서드 레벨:

@Get('profile')
@UseGuards(AuthGuard)
getProfile() {
  return 'This is protected';
}

전역 레벨:

// app.module.ts
import { APP_GUARD } from '@nestjs/core';
 
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

NestJS 공식 문서에서 "You can set up a guard globally from any module using APP_GUARD from '@nestjs/core', making it apply to all routes in your application" 라고 설명합니다. [GeeksforGeeks - NestJS Guards] (opens in a new tab)

CanActivate vs AuthGuard: 무엇을 사용해야 할까?

Guard를 구현하는 방법은 크게 두 가지입니다. CanActivate 인터페이스를 직접 구현하거나, AuthGuard를 상속하는 방식입니다. 이 두 가지 방식의 차이를 명확히 이해하는 것이 중요합니다.

CanActivate 인터페이스 직접 구현

정의 및 특징:

CanActivate 인터페이스를 직접 구현하는 방식은 Passport 없이 완전히 커스텀 인증/인가 로직을 작성하는 방법입니다. 모든 검증 로직을 직접 제어할 수 있습니다.

NestJS 공식 문서에서 "Guards implement the CanActivate interface and have a single responsibility: they determine whether a given request will be handled by the route handler or not" 라고 명시되어 있습니다. [NestJS Guards 공식 문서] (opens in a new tab)

구현 예시:

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
 
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
 
    if (!request.user) {
      throw new UnauthorizedException('사용자 인증이 필요합니다');
    }
 
    return request.user ? true : false;
  }
}

사용 시나리오:

  • 단순한 권한 검사 (예: request.user 존재 여부만 확인)
  • 비즈니스 로직 기반 접근 제어 (예: 특정 시간대에만 접근 허용)
  • Passport가 필요 없는 경우
  • 역할 기반 권한 검사 (RolesGuard)

DigitalOcean 튜토리얼에서 "Every guard created in NestJS must implement the canActivate function, which returns a boolean that specifies if the current request should go through or not" 라고 설명합니다. [DigitalOcean - Understanding Guards in NestJS] (opens in a new tab)

장점:

  • 완전한 제어권: 모든 로직을 원하는 대로 구현 가능
  • 가벼운 구현: Passport 의존성 없이 필요한 기능만 구현
  • 명확한 의도: 코드만 보고도 정확히 무엇을 검사하는지 파악 가능

단점:

  • 모든 로직을 직접 작성해야 함
  • 토큰 추출, 검증, 에러 처리 등을 수동으로 구현
  • 표준 인증 메커니즘(JWT, OAuth) 사용 시 보일러플레이트 코드 증가

AuthGuard 상속 (Passport 통합)

정의 및 특징:

@nestjs/passportAuthGuard를 상속하는 방식은 Passport.js Strategy와 자동으로 연동됩니다. AuthGuard는 내부적으로 passport.authenticate()를 호출하여 해당 Strategy를 실행합니다.

Stack Overflow에서 "AuthGuard() takes in a strategy name and returns a guard class which has its own canActivate method and calls passport.authenticate(strategy)" 라고 설명되어 있습니다. [Stack Overflow - Guards and Strategies 동작 원리] (opens in a new tab)

구현 예시:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

사용 예시:

@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
  return req.user; // JwtStrategy의 validate()가 반환한 값
}

내부 동작 흐름:

  1. JwtAuthGuardcanActivate() 실행
  2. 내부적으로 passport.authenticate('jwt') 호출
  3. JwtStrategyvalidate() 메서드 실행
  4. validate() 반환값을 request.user에 자동 설정
  5. true 반환 시 핸들러 실행, false 시 401 에러

사용 시나리오:

  • JWT, OAuth, Local 등 표준 인증 메커니즘 사용
  • 토큰 추출 및 검증이 필요한 경우
  • Passport Strategy와 함께 사용하는 경우
  • 검증된 인증 패턴 활용

장점:

  • 검증된 패턴: 업계 표준 인증 방식 활용
  • 적은 코드: 대부분의 로직이 내장되어 있음
  • Strategy 자동 연동: validate() 메서드만 구현하면 됨
  • 수백 개의 Passport 플러그인 활용 가능

단점:

  • Passport 의존성 필요
  • Strategy와 함께 사용해야 하므로 추가 설정 필요
  • 내부 동작을 이해하지 못하면 디버깅이 어려울 수 있음

혼합 패턴: AuthGuard 상속 + canActivate 오버라이드

가장 강력한 패턴은 AuthGuard를 상속하면서 canActivate()를 오버라이드하는 것입니다. 이를 통해 Passport의 인증 메커니즘과 커스텀 로직을 결합할 수 있습니다.

구현 예시 1: Public 데코레이터 지원

import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
 
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
 
    if (isPublic) {
      return true; // Public 라우트는 인증 건너뛰기
    }
 
    return super.canActivate(context); // Passport 인증 실행
  }
}

Stack Overflow에서 "You can override canActivate() by calling super.canActivate(context) to invoke the parent class (AuthGuard) method while adding your custom logic" 라고 설명되어 있습니다. [Stack Overflow - canActivate 오버라이드] (opens in a new tab)

구현 예시 2: 추가 검증 로직

@Injectable()
export class CustomJwtGuard extends AuthGuard('jwt') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 먼저 Passport JWT 인증 실행
    const parentCanActivate = (await super.canActivate(context)) as boolean;
 
    if (!parentCanActivate) {
      return false;
    }
 
    // 인증 후 추가 비즈니스 로직 검증
    const request = context.switchToHttp().getRequest();
    const user = request.user; // JwtStrategy.validate()가 반환한 값
 
    // 예: 계정 활성화 여부 확인
    if (!user.isActive) {
      throw new UnauthorizedException('비활성화된 계정입니다');
    }
 
    return true;
  }
}

핵심 포인트:

super.canActivate(context)를 호출해야 Passport의 authenticate() 메서드가 실행되어 Strategy의 validate()가 호출됩니다. 이를 생략하면 인증이 작동하지 않습니다.

Stack Overflow에서 "super.canActivate() is crucial because it triggers passport.authenticate() which executes the JwtStrategy's validate method and populates req.user" 라고 명시되어 있습니다. [Stack Overflow - super.canActivate 중요성] (opens in a new tab)

비교표

구분CanActivate 직접 구현AuthGuard 상속AuthGuard + canActivate 오버라이드
Passport 의존성없음필요필요
Strategy 연동불가자동자동
구현 복잡도높음 (모두 직접 구현)낮음 (내장 기능)중간 (일부 커스텀)
유연성최대낮음높음
사용 사례커스텀 권한 검사JWT/OAuth 인증인증 + 추가 검증
코드량많음매우 적음적음
표준 패턴비표준표준표준 + 확장

실전 선택 가이드

CanActivate 직접 구현을 선택하는 경우:

// 예시 1: 역할 기반 권한 검사
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    const request = context.switchToHttp().getRequest();
    const user = request.user;
 
    return roles.some(role => user.roles?.includes(role));
  }
}
 
// 예시 2: 단순 사용자 존재 여부 검사
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return !!request.user;
  }
}

AuthGuard 상속을 선택하는 경우:

// JWT 인증
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
 
// Local 인증 (username/password)
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

혼합 패턴을 선택하는 경우:

// 전역 JWT 인증 + Public 라우트 지원
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }
 
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.get('isPublic', context.getHandler());
    if (isPublic) return true;
 
    return super.canActivate(context);
  }
}

AuthGuard vs PassportStrategy: 역할의 차이

여기까지 CanActivateAuthGuard의 차이를 살펴봤습니다. 이제 많은 개발자가 혼동하는 AuthGuardPassportStrategy의 차이를 명확히 해보겠습니다.

핵심 차이점

중요: AuthGuard를 상속하는 것과 PassportStrategy를 상속하는 것은 완전히 다른 역할을 수행합니다.

구분extends AuthGuardextends PassportStrategy
역할Guard (언제 인증할지)Strategy (어떻게 인증할지)
목적Strategy 실행 및 적용인증 로직 구현
핵심 메서드canActivate()validate()
등록 위치컨트롤러/메서드 (@UseGuards)모듈 (providers)
내부 동작passport.authenticate() 호출실제 인증 검증 수행
예시JwtAuthGuard, LocalAuthGuardJwtStrategy, LocalStrategy

PassportStrategy 상속 (Strategy 정의)

역할: 인증 로직을 구현하는 클래스입니다. "어떻게 인증할 것인가"를 정의합니다.

Stack Overflow에서 "PassportStrategy is a mixin that calls passport.use(new Strategy(this.validate.bind(this))) under the hood, which configures Passport and makes it aware of the strategy" 라고 설명합니다. [Stack Overflow - AuthGuard vs PassportStrategy] (opens in a new tab)

구현 예시:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';
 
@Injectable()
export class WebSessionAuthStrategy extends PassportStrategy(
  Strategy,
  'web-session', // Strategy 이름
) {
  constructor(private authService: AuthService) {
    super({
      usernameField: 'email', // 기본값은 'username'
      passwordField: 'password',
    });
  }
 
  async validate(email: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(email, password);
    if (!user) {
      throw new UnauthorizedException('인증 정보가 올바르지 않습니다');
    }
    return user; // 이 값이 request.user에 설정됨
  }
}

등록 방법:

@Module({
  providers: [WebSessionAuthStrategy], // providers에 등록
})
export class AuthModule {}

핵심 특징:

  • validate() 메서드에서 실제 인증 수행
  • 반환값이 자동으로 request.user에 설정됨
  • 두 번째 인자 ('web-session')로 Strategy 이름 지정
  • 이 이름을 AuthGuard에서 참조함

NestJS 공식 문서에서 "The validate() method's return value will be attached to the request object (by default under the property user)" 라고 명시되어 있습니다. [NestJS Authentication 공식 문서] (opens in a new tab)

AuthGuard 상속 (Guard 정의)

역할: Strategy를 실행하는 클래스입니다. "언제 인증할 것인가"를 결정합니다.

Stack Overflow에서 "AuthGuard() takes in a strategy name and returns a guard class which calls passport.authenticate(strategy)" 라고 설명합니다. [Stack Overflow - How AuthGuard Works] (opens in a new tab)

구현 예시:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class WebSessionAuthGuard extends AuthGuard('web-session') {}
// 'web-session' 이름의 Strategy를 실행

적용 방법:

@Controller('auth')
export class AuthController {
  @Post('login')
  @UseGuards(WebSessionAuthGuard) // Guard 적용
  async login(@Request() req) {
    return req.user; // WebSessionAuthStrategy.validate()가 반환한 값
  }
}

핵심 특징:

  • @UseGuards() 데코레이터로 컨트롤러/메서드에 적용
  • 내부적으로 passport.authenticate('web-session') 호출
  • 해당 이름의 Strategy를 찾아서 실행
  • Strategy의 validate() 결과에 따라 요청 허용/거부

둘의 관계: 협력 구조

StrategyGuard는 서로 협력하여 인증을 수행합니다.

// 1단계: Strategy 정의 (인증 로직)
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secret',
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
 
// 2단계: Guard 정의 (Strategy 참조)
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
 
// 3단계: 컨트롤러에 적용
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
  return req.user; // JwtStrategy.validate()가 반환한 값
}

실행 흐름:

  1. 요청 수신: 클라이언트가 /profile에 요청
  2. Guard 실행: JwtAuthGuard.canActivate() 실행
  3. Strategy 호출: 내부적으로 passport.authenticate('jwt') 호출
  4. Strategy 검색: 'jwt' 이름을 가진 JwtStrategy 검색
  5. validate 실행: JwtStrategy.validate() 메서드 실행
  6. 사용자 설정: validate() 반환값을 request.user에 저장
  7. 핸들러 실행: Guard가 true 반환 시 컨트롤러 핸들러 실행

Medium 블로그에서 "PassportStrategy defines the authentication logic, while AuthGuard applies that logic to protect routes" 라고 설명합니다. [Medium - Definitive Guide for Guards and Passport] (opens in a new tab)

Strategy는 필수, Guard는 선택사항?

Strategy는 반드시 필요합니다. 인증 로직이 없으면 인증을 수행할 수 없기 때문입니다.

Guard 클래스는 선택사항입니다. AuthGuard('strategy-name')을 직접 사용할 수 있습니다.

Guard 클래스 없이 사용:

@Get('profile')
@UseGuards(AuthGuard('jwt')) // 직접 사용
getProfile(@Request() req) {
  return req.user;
}

Guard 클래스를 만드는 이유:

// 장점 1: 타입 안정성 (문자열 오타 방지)
@UseGuards(JwtAuthGuard) // vs @UseGuards(AuthGuard('jwt'))
 
// 장점 2: 커스텀 로직 추가 가능
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Public 라우트 체크 등 추가 로직
    return super.canActivate(context);
  }
}
 
// 장점 3: 코드 일관성 및 재사용성

Stack Overflow에서 "You don't have to create a custom AuthGuard. You should only create it if you don't want to continually use a string to remember what strategy you're using" 라고 설명합니다. [Stack Overflow - Do You Need Custom AuthGuard] (opens in a new tab)

실전 사례: 전체 구현

1단계: Strategy 구현

// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

2단계: Guard 구현 (선택사항)

// jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.get('isPublic', context.getHandler());
    if (isPublic) return true;
 
    return super.canActivate(context);
  }
}

3단계: 모듈 등록

// auth.module.ts
@Module({
  imports: [
    PassportModule,
    JwtModule.register({ secret: process.env.JWT_SECRET }),
  ],
  providers: [JwtStrategy], // Strategy는 반드시 등록
})
export class AuthModule {}

4단계: 컨트롤러에 적용

// user.controller.ts
@Controller('users')
export class UserController {
  @Get('profile')
  @UseGuards(JwtAuthGuard) // 또는 AuthGuard('jwt')
  getProfile(@Request() req) {
    return req.user;
  }
}

요약: 언제 무엇을 사용하는가?

PassportStrategy를 상속할 때:

  • 새로운 인증 메커니즘을 추가할 때
  • JWT, Local, OAuth 등의 인증 로직 구현
  • validate() 메서드에 인증 로직 작성
  • 모듈의 providers에 등록

AuthGuard를 상속할 때:

  • 기존 Strategy를 사용하여 라우트를 보호할 때
  • 커스텀 로직 추가가 필요한 경우 (Public 데코레이터 등)
  • 타입 안정성과 코드 재사용성을 높이고 싶을 때
  • @UseGuards()로 컨트롤러/메서드에 적용

둘의 관계:

  • Strategy = 재사용 가능한 인증 로직 (How)
  • Guard = 적용 지점 (When)
  • 하나의 Strategy를 여러 Guard에서 재사용 가능

Strategy란 무엇인가?

정의 및 역할

Strategy는 Passport.js와 NestJS의 통합을 통해 인증 메커니즘을 정의하는 클래스입니다. 각 Strategy는 고유한 이름을 가지며(예: 'local', 'jwt'), 해당 이름으로 Guard에서 호출됩니다.

Stack Overflow에서 "Each Strategy from a passport-* package has a name property - for passport-local that name is 'local', and for passport-jwt, that name is 'jwt'" 라고 설명되어 있습니다. [Stack Overflow - NestJS AuthGuard와 Passport Strategy] (opens in a new tab)

Local Strategy 구현 예시

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
 
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }
 
  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

JWT Strategy 구현 예시

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'yourSecretKey',
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

validate 메서드의 역할

validate() 메서드는 Strategy의 핵심입니다. 이 메서드의 반환값은 자동으로 request.user에 할당되어 컨트롤러에서 사용할 수 있습니다.

Guard와 Strategy의 연동

AuthGuard 사용법

NestJS는 @nestjs/passport 패키지를 통해 Passport Strategy를 Guard로 쉽게 사용할 수 있도록 AuthGuard를 제공합니다.

직접 사용:

@Get('profile')
@UseGuards(AuthGuard('jwt'))
getProfile(@Request() req) {
  return req.user;
}

커스텀 Guard로 확장:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
  return req.user;
}

내부 동작 흐름

  1. 요청 수신: 클라이언트가 보호된 엔드포인트에 요청
  2. Guard 실행: AuthGuard('jwt')canActivate() 메서드 실행
  3. Strategy 호출: 내부적으로 passport.authenticate('jwt') 호출
  4. 토큰 추출 및 검증: JwtStrategyExtractJwt가 토큰 추출, 서명 검증
  5. validate 실행: JwtStrategyvalidate(payload) 메서드 실행
  6. 사용자 객체 설정: validate 반환값을 request.user에 저장
  7. 핸들러 실행: Guard가 true 반환 시 라우트 핸들러 실행

Stack Overflow에서 "Whether you use class LocalAuthGuard extends AuthGuard('local') or AuthGuard('local'), the same thing is happening with regards to passport and the LocalStrategy. The guard invokes the strategy during the authentication process" 라고 설명되어 있습니다. [Stack Overflow - AuthGuard와 PassportStrategy 차이] (opens in a new tab)

실전 사용 패턴

패턴 1: Login + JWT 인증 플로우

이것은 가장 일반적인 인증 패턴입니다.

1단계: 로그인 엔드포인트 (Local Strategy 사용)

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
 
  @Post('login')
  @UseGuards(AuthGuard('local'))
  async login(@Request() req) {
    // req.user는 LocalStrategy의 validate에서 반환한 값
    return this.authService.login(req.user);
  }
}

Medium 블로그에서 "Use local authentication to validate a user's credentials and issue a JWT token upon successful login" 라고 설명합니다. [Medium - Local vs JWT AuthGuard] (opens in a new tab)

2단계: 보호된 리소스 (JWT Strategy 사용)

@Controller('users')
export class UsersController {
  @Get('profile')
  @UseGuards(AuthGuard('jwt'))
  getProfile(@Request() req) {
    return req.user;
  }
}

패턴 2: 다중 Guard 조합

Guard는 여러 개를 순서대로 적용할 수 있습니다.

@Get('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
getAdminData() {
  return 'Admin data';
}
  • JwtAuthGuard: 먼저 JWT 토큰을 검증하여 인증 확인
  • RolesGuard: 인증된 사용자의 역할을 확인하여 권한 검사

Medium 블로그에서 "Multiple guards will succeed as long as all of them pass, with typical use cases including reusing different combinations of guards on different controller routes" 라고 설명되어 있습니다. [Medium - Guards vs Middlewares vs Interceptors] (opens in a new tab)

RolesGuard 구현 예시:

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) {
      return true;
    }
 
    const request = context.switchToHttp().getRequest();
    const user = request.user;
 
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

패턴 3: 전역 Guard + Public 데코레이터

모든 라우트에 인증을 적용하되, 특정 라우트는 예외로 처리하는 패턴입니다.

전역 Guard 설정:

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

Public 데코레이터 정의:

import { SetMetadata } from '@nestjs/common';
 
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

JwtAuthGuard 수정:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private 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);
  }
}

사용 예시:

@Controller('auth')
export class AuthController {
  @Public()
  @Post('login')
  login() {
    // 인증 없이 접근 가능
  }
}

Guard vs Strategy: 언제 무엇을 사용할까?

Guard를 사용하는 경우

  • 접근 제어가 필요한 경우: 특정 조건에 따라 라우트 접근을 허용/거부
  • 역할 기반 권한 검사: 이미 인증된 사용자의 역할을 확인
  • 커스텀 비즈니스 로직: Passport와 무관한 자체 인증/인가 로직

Medium 블로그에서 "Use Guards when validating if a given user has the right to use a given method" 라고 설명합니다. [Medium - Definitive Guide for NestJS Guards and Passport] (opens in a new tab)

Strategy를 사용하는 경우

  • 인증 메커니즘 정의가 필요한 경우: JWT 검증, OAuth, 로컬 인증 등
  • Passport 플러그인 활용: 수백 개의 Passport 전략 중 선택하여 사용
  • 표준화된 인증 패턴: 업계 표준 인증 방식 구현

Medium 블로그에서 "Use Strategies when defining authentication logic (JWT validation, local username/password, OAuth, etc.)" 라고 설명되어 있습니다. [Medium - Definitive Guide for NestJS Guards and Passport] (opens in a new tab)

요청 처리 순서

NestJS 요청 처리 파이프라인에서의 순서:

  1. Middleware: 요청 전처리 (로깅, CORS 등)
  2. Guard: 인증/인가 검사
  3. Interceptor (pre-controller): 요청 변환
  4. Pipe: 요청 데이터 검증 및 변환
  5. Controller: 비즈니스 로직 실행
  6. Interceptor (post-controller): 응답 변환

Medium 블로그에서 "When a request comes into a NestJS application, it flows through: Middleware, then Guard, then Interceptor (pre-controller)" 라고 명시되어 있습니다. [Medium - NestJS Request Flow] (opens in a new tab)

모듈 설정

Guard와 Strategy를 사용하려면 적절한 모듈에 등록해야 합니다.

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
 
@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'yourSecretKey',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

필수 패키지:

npm install @nestjs/passport passport passport-local passport-jwt
npm install -D @types/passport-local @types/passport-jwt

NestJS 공식 문서에서 "For implementing authentication with Passport in NestJS, you typically need: passport, @nestjs/passport, and strategy-specific packages like passport-local or passport-jwt" 라고 명시되어 있습니다. [NestJS Passport Recipe] (opens in a new tab)

주의사항 및 Best Practices

1. Strategy 이름 일치

Guard와 Strategy의 이름은 반드시 일치해야 합니다.

// Strategy에서 'jwt' 사용
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {}
 
// Guard에서도 'jwt' 사용
@UseGuards(AuthGuard('jwt'))

2. Strategy는 Provider로 등록

Strategy는 반드시 모듈의 providers 배열에 등록해야 합니다.

@Module({
  providers: [JwtStrategy, LocalStrategy], // 필수
})

3. 민감 정보 환경 변수 관리

JWT Secret 등 민감 정보는 환경 변수로 관리해야 합니다.

JwtModule.register({
  secret: process.env.JWT_SECRET,
  signOptions: { expiresIn: '1h' },
})

4. Guard에서 예외 처리

Strategy에서 발생한 예외는 Guard가 처리합니다. 명시적인 예외 처리가 필요한 경우 Guard를 확장하세요.

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  handleRequest(err, user, info) {
    if (err || !user) {
      throw err || new UnauthorizedException('Invalid token');
    }
    return user;
  }
}

참고 자료