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.)" 라고 명시되어 있습니다.
- Strategy는 Passport를 통해 실제 인증 로직을 정의하는 클래스입니다.
PassportStrategy를 상속하며,validate()메서드에서 인증 처리 후 사용자 객체를 반환합니다 [NestJS Authentication 공식 문서] (opens in a new tab)
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" 라고 설명합니다.
- Guard와 Strategy의 관계: Guard는 Strategy를 실행하는 래퍼입니다.
AuthGuard('jwt')는 내부적으로passport.authenticate('jwt')를 호출하여 해당 이름의 Strategy를 실행합니다 [Stack Overflow - NestJS Guard와 Strategy 동작 원리] (opens in a new tab)
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/passport의 AuthGuard를 상속하는 방식은 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()가 반환한 값
}내부 동작 흐름:
JwtAuthGuard의canActivate()실행- 내부적으로
passport.authenticate('jwt')호출 JwtStrategy의validate()메서드 실행validate()반환값을request.user에 자동 설정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: 역할의 차이
여기까지 CanActivate와 AuthGuard의 차이를 살펴봤습니다. 이제 많은 개발자가 혼동하는 AuthGuard와 PassportStrategy의 차이를 명확히 해보겠습니다.
핵심 차이점
중요: AuthGuard를 상속하는 것과 PassportStrategy를 상속하는 것은 완전히 다른 역할을 수행합니다.
| 구분 | extends AuthGuard | extends PassportStrategy |
|---|---|---|
| 역할 | Guard (언제 인증할지) | Strategy (어떻게 인증할지) |
| 목적 | Strategy 실행 및 적용 | 인증 로직 구현 |
| 핵심 메서드 | canActivate() | validate() |
| 등록 위치 | 컨트롤러/메서드 (@UseGuards) | 모듈 (providers) |
| 내부 동작 | passport.authenticate() 호출 | 실제 인증 검증 수행 |
| 예시 | JwtAuthGuard, LocalAuthGuard | JwtStrategy, 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()결과에 따라 요청 허용/거부
둘의 관계: 협력 구조
Strategy와 Guard는 서로 협력하여 인증을 수행합니다.
// 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()가 반환한 값
}실행 흐름:
- 요청 수신: 클라이언트가
/profile에 요청 - Guard 실행:
JwtAuthGuard.canActivate()실행 - Strategy 호출: 내부적으로
passport.authenticate('jwt')호출 - Strategy 검색: 'jwt' 이름을 가진
JwtStrategy검색 - validate 실행:
JwtStrategy.validate()메서드 실행 - 사용자 설정:
validate()반환값을request.user에 저장 - 핸들러 실행: 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;
}내부 동작 흐름
- 요청 수신: 클라이언트가 보호된 엔드포인트에 요청
- Guard 실행:
AuthGuard('jwt')의canActivate()메서드 실행 - Strategy 호출: 내부적으로
passport.authenticate('jwt')호출 - 토큰 추출 및 검증:
JwtStrategy의ExtractJwt가 토큰 추출, 서명 검증 - validate 실행:
JwtStrategy의validate(payload)메서드 실행 - 사용자 객체 설정:
validate반환값을request.user에 저장 - 핸들러 실행: 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 요청 처리 파이프라인에서의 순서:
- Middleware: 요청 전처리 (로깅, CORS 등)
- Guard: 인증/인가 검사
- Interceptor (pre-controller): 요청 변환
- Pipe: 요청 데이터 검증 및 변환
- Controller: 비즈니스 로직 실행
- 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-jwtNestJS 공식 문서에서 "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;
}
}참고 자료
- NestJS Guards 공식 문서 (opens in a new tab)
- NestJS Authentication 공식 문서 (opens in a new tab)
- NestJS Passport Recipe (opens in a new tab)
- NestJS Passport GitHub (opens in a new tab)
- Stack Overflow - Guards and Strategies 내부 동작 (opens in a new tab)
- Medium - Definitive Guide for NestJS Guards and Passport (opens in a new tab)
- Medium - NestJS Request Flow (opens in a new tab)