Nest.js를 배워보자/7. NestJS 인증 시스템 구축 — JWT & Passport 실전

Passport Strategy 구현: Local & JWT 실전

_Blue_Sky_ 2025. 12. 2. 19:57
728x90

 


NestJS에서 Passport 모듈은 인증 방식을 Strategy(전략)라는 플러그인 형태로 분리하여 사용합니다. 우리는 로그인 시점과 로그인 후 인증이 필요한 모든 요청에 대해 두 가지 핵심 전략을 구현해야 합니다.

1. 🛡️ Local Strategy 구현 (로그인)

Local Strategy는 사용자 이름(또는 이메일)과 비밀번호를 서버에서 검증하여 사용자를 인증하는 데 사용됩니다. 이는 주로 POST /auth/login 엔드포인트에서 사용됩니다.

1.1. 파일 구조 및 상속

@nestjs/passport에서 제공하는 PassportStrategy 헬퍼 클래스를 사용하여 기본 passport-local 라이브러리의 Strategy를 상속받습니다.

// src/auth/strategies/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';

@Injectable()
// 💡 PassportStrategy(Strategy)를 상속받아 LocalStrategy 정의
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    // 💡 super()를 통해 Local Strategy 기본 설정 전달
    super({
      // 💡 사용자 이름 필드를 'email'로 사용하도록 지정 (기본값은 'username')
      usernameField: 'email', 
    });
  }

  // 💡 핵심 검증 메서드
  // Passport가 호출하며, 클라이언트가 보낸 email과 password가 인자로 전달됨
  async validate(email: string, password: string): Promise<any> {
    // 1. AuthService를 통해 DB에서 사용자 검증 로직 수행
    const user = await this.authService.validateUser(email, password);

    // 2. 검증 실패 시:
    if (!user) {
      throw new UnauthorizedException('제공된 자격 증명이 유효하지 않습니다.');
    }

    // 3. 검증 성공 시:
    // 💡 반환된 user 객체는 Local Guard가 실행된 후 
    //    요청 객체(req)의 'user' 속성에 할당됩니다 (req.user = user).
    return user; 
  }
}
728x90

1.2. Local Guard를 통한 사용

컨트롤러에서 AuthGuard('local')을 사용하면 NestJS가 내부적으로 LocalStrategy의 validate() 메서드를 실행시킵니다. 

// src/auth/auth.controller.ts (로그인 엔드포인트)
import { AuthGuard } from '@nestjs/passport';
// ...
@UseGuards(AuthGuard('local')) // 💡 LocalStrategy 실행 지시
@Post('login')
async login(@Request() req) {
  // LocalStrategy가 성공적으로 반환한 user 객체가 req.user에 담겨 있습니다.
  return this.authService.login(req.user); // 💡 JWT 토큰을 생성하고 반환
}

2. 🔑 JWT Strategy 구현 (인증)

JWT Strategy는 클라이언트가 로그인 후 요청 헤더에 담아 보낸 Access Token을 검증하는 데 사용됩니다. 이는 인증이 필요한 모든 API 엔드포인트에서 사용됩니다.

2.1. 파일 구조 및 설정

@nestjs/passport의 PassportStrategy를 사용하여 passport-jwt 라이브러리의 Strategy를 상속받습니다. JWT 전략은 super() 생성자에서 토큰 추출 방식과 비밀 키를 설정하는 것이 중요합니다. 

// src/auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
// 💡 PassportStrategy(Strategy)를 상속받아 JWTStrategy 정의
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      // 💡 1. 토큰 추출 방법: Authorization 헤더의 Bearer 스킴에서 추출
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 
      
      // 💡 2. 서명 무시 여부: false로 설정하여, 서버가 토큰 서명을 검증하도록 함
      ignoreExpiration: false, 
      
      // 💡 3. 비밀 키: 환경 변수에서 JWT 비밀 키를 가져와 서명 검증에 사용
      secretOrKey: configService.get<string>('JWT_SECRET'), 
    });
  }

  // 💡 핵심 검증 메서드
  // 토큰이 유효하고 서명이 일치하면, 복호화된 Payload가 인자로 전달됨
  async validate(payload: any) {
    // 1. Payload를 통해 추가 DB 조회 (선택 사항)
    //    예: 토큰 Payload의 userId를 이용해 DB에서 최신 사용자 정보를 다시 가져올 수 있음
    
    // 2. 💡 최종적으로 반환된 객체는 JWT Guard가 실행된 후 
    //    요청 객체(req)의 'user' 속성에 할당됩니다 (req.user = { userId: 1, email: '...' }).
    return { userId: payload.sub, email: payload.email }; 
  }
}
728x90

2.2. JWT Guard를 통한 사용

인증된 사용자만 접근할 수 있는 라우트에 AuthGuard('jwt')를 적용합니다. 

// src/users/users.controller.ts
import { AuthGuard } from '@nestjs/passport';
// ...
@UseGuards(AuthGuard('jwt')) // 💡 JwtStrategy 실행 지시
@Get('profile')
getProfile(@Request() req) {
  // JwtStrategy에서 반환된 Payload 정보가 req.user에 담겨 있습니다.
  return `사용자 ID: ${req.user.userId}, 이메일: ${req.user.email}`;
}

3. 🥇 Passport 전략의 이점

두 전략을 분리하여 사용함으로써 NestJS는 인증/인가 로직을 모듈화하고, 각 라우트의 성격에 따라 필요한 인증 레벨(로그인 또는 인증됨)을 명확하게 적용할 수 있습니다.


728x90