icon
4장 : 인증과 권한 부여

Passport를 이용한 인증 구현


안녕하세요! 3장에서는 NestJS 애플리케이션의 데이터를 안전하게 관리하는 방법에 대해 다루었습니다. 이제 4장에서는 웹 애플리케이션의 또 다른 핵심 요소인 인증(Authentication)과 권한 부여(Authorization) 에 대해 알아보겠습니다. 사용자 로그인, 개인 정보 보호, 특정 기능 접근 제한 등 모든 현대 웹 서비스에서 인증과 권한 부여는 필수적입니다.

NestJS는 Node.js 생태계의 인기 있는 인증 미들웨어인 Passport.js와의 완벽한 통합을 통해 다양한 인증 전략을 손쉽게 구현할 수 있도록 지원합니다. 이번 절에서는 Passport의 기본 개념을 이해하고, 이를 NestJS에 적용하여 사용자 인증을 구현하는 방법에 대해 상세히 살펴보겠습니다.


인증과 권한 부여란?

두 용어는 자주 혼용되지만, 명확히 다른 개념을 가지고 있습니다.

  • 인증(Authentication): "당신이 누구인지 확인하는 과정" 입니다. 사용자가 주장하는 신원(ID)이 진짜인지 확인하는 절차입니다. 예를 들어, 아이디와 비밀번호를 통해 로그인하는 것이 인증의 대표적인 예입니다. (예: "로그인하셨군요, 환영합니다!")
  • 권한 부여(Authorization): "당신이 무엇을 할 수 있는지 결정하는 과정" 입니다. 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 절차입니다. 예를 들어, 일반 사용자는 게시물을 읽을 수 있지만, 관리자만 게시물을 삭제할 수 있는 것이 권한 부여의 예입니다. (예: "관리자만 이 기능을 사용할 수 있습니다.")

이번 절에서는 주로 인증에 초점을 맞추고, 다음 절에서 권한 부여에 대해 더 자세히 다루겠습니다.


Passport.js란 무엇이며 왜 사용할까요?

Passport.js는 Node.js를 위한 강력하고 유연한 인증 미들웨어입니다. 웹 애플리케이션에 다양한 인증 전략(로컬 인증, JWT, OAuth, 소셜 로그인 등)을 쉽게 통합할 수 있도록 설계되었습니다. Passport는 인증 자체의 복잡한 로직을 추상화하여 개발자가 인증 방식에 대한 고민 없이 핵심 비즈니스 로직에 집중할 수 있도록 돕습니다.

Passport 사용의 주요 장점

  • 모듈화된 전략: Passport는 '전략(Strategy)'이라는 개념을 사용하여 다양한 인증 방식을 플러그인 형태로 제공합니다. 로컬(아이디/비밀번호), JWT, OAuth2 (Google, Facebook, GitHub 등), OpenID 등 수백 가지의 전략 중에서 필요한 것을 선택하여 적용할 수 있습니다.
  • 유연하고 확장 가능: 특정 인증 방식에 얽매이지 않고, 필요에 따라 커스텀 전략을 만들거나 여러 전략을 조합하여 사용할 수 있습니다.
  • NestJS와의 통합 용이성: @nestjs/passport 패키지를 통해 NestJS의 의존성 주입 시스템과 완벽하게 통합되어 있습니다.

Passport를 이용한 로컬(Local) 인증 구현

가장 기본적인 인증 방식인 로컬 인증(아이디와 비밀번호 사용) 을 NestJS와 Passport를 사용하여 구현해 보겠습니다.

단계 1: 필요한 패키지 설치

npm install @nestjs/passport passport passport-local @types/passport-local
npm install --save-dev @types/passport # passport 타입 정의도 설치

단계 2: User 모듈 및 서비스 준비 (가정)

사용자 정보를 관리하는 UsersModule, UsersService가 이미 있다고 가정합니다. UsersService는 사용자 정보(여기서는 간단히 usernamepassword)를 데이터베이스(또는 메모리)에서 조회하는 기능을 포함합니다.

src/users/users.service.ts
import { Injectable } from '@nestjs/common';

// 실제 DB 연동 로직 대신 간단한 사용자 데이터 배열 사용
const users = [
  { userId: 1, username: 'testuser', password: 'password123', roles: ['user'] },
  { userId: 2, username: 'admin', password: 'adminpassword', roles: ['admin'] },
];

@Injectable()
export class UsersService {
  async findOne(username: string): Promise<any | undefined> {
    return users.find(user => user.username === username);
  }

  // 실제로는 비밀번호 해싱 및 검증 로직이 필요합니다.
  // 여기서는 단순히 password 문자열 일치 여부만 확인합니다.
  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user; // 비밀번호 제외하고 반환
      return result;
    }
    return null;
  }
}

단계 3: Passport 전략 구현 (LocalStrategy)

Passport는 인증 로직을 '전략'으로 캡슐화합니다. 로컬 인증을 위한 LocalStrategy를 생성합니다.

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private usersService: UsersService) {
    super(); // Passport-local의 기본 옵션 사용
  }

  // validate 메서드는 사용자 이름과 비밀번호를 받아 유효성을 검증합니다.
  async validate(username: string, password: string): Promise<any> {
    const user = await this.usersService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials'); // 인증 실패 시 예외 발생
    }
    return user; // 유효한 사용자일 경우, user 객체를 반환합니다.
                // 이 user 객체는 Request 객체의 req.user에 주입됩니다.
  }
}

단계 4: 인증 모듈(AuthModule) 생성

인증 관련 로직을 하나의 모듈로 묶습니다.

src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { UsersModule } from '../users/users.module'; // UsersModule 임포트
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';

@Module({
  imports: [UsersModule, PassportModule], // UsersModule과 PassportModule 임포트
  providers: [AuthService, LocalStrategy], // 인증 서비스와 전략 등록
  controllers: [AuthController], // 인증 컨트롤러 등록
})
export class AuthModule {}
  • AuthService: 로그인 등의 인증 로직을 수행하는 서비스 (선택 사항이지만, 컨트롤러에서 로직을 분리하는 데 유용).
  • PassportModule: Passport 기능을 NestJS에 통합하는 모듈.

단계 5: 인증 서비스(AuthService) 작성

실제 로그인 로직을 처리하는 서비스를 작성합니다. 여기서는 validateUser를 호출하고, 성공 시 JWT 토큰 등을 발행하는 역할을 할 수 있습니다. 지금은 단순히 사용자 정보를 반환합니다.

src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  // 실제 JWT 발급 로직은 다음 절에서 다룰 예정입니다.
  async login(user: any) {
    return user;
  }
}

단계 6: 인증 컨트롤러(AuthController) 작성

클라이언트의 로그인 요청을 처리하는 엔드포인트를 만듭니다. @UseGuards(AuthGuard('local'))를 사용하여 Passport의 Local 전략을 활성화합니다.

src/auth/auth.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; // AuthGuard 임포트
import { AuthService } from './auth.service';

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

  // @UseGuards(AuthGuard('local'))를 통해 LocalStrategy가 실행됩니다.
  // LocalStrategy의 validate() 메서드가 성공하면 req.user에 사용자 정보가 주입됩니다.
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    // Passport LocalStrategy에 의해 검증된 사용자 정보는 req.user에 있습니다.
    // 여기서는 AuthService의 login 메서드를 호출하여 토큰 발급 등의 추가 로직을 수행할 수 있습니다.
    return this.authService.login(req.user);
  }
}

단계 7: Root 모듈에 AuthModule 임포트

AppModuleAuthModule을 임포트하여 애플리케이션이 인증 기능을 사용할 수 있도록 합니다.

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module'; // AuthModule 임포트
import { UsersModule } from './users/users.module';

@Module({
  imports: [AuthModule, UsersModule], // AuthModule과 UsersModule 임포트
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

테스트해보기

애플리케이션을 실행합니다. npm run start:dev

Postman이나 curl을 사용하여 http://localhost:3000/auth/login으로 POST 요청을 보냅니다.

  • Bodyx-www-form-urlencoded 또는 raw (JSON) 형태로 usernamepassword를 포함해야 합니다.
    {
        "username": "testuser",
        "password": "password123"
    }
  • x-www-form-urlencoded를 사용하는 경우: username=testuser password=password123

성공 시: { "userId": 1, "username": "testuser", "roles": ["user"] }와 유사한 응답을 받게 됩니다 (AuthService의 login 메서드 반환 값).

실패 시 (잘못된 비밀번호): {"statusCode":401,"message":"Unauthorized","error":"Unauthorized"}와 같은 401 Unauthorized 응답을 받게 됩니다.


이것으로 NestJS와 Passport를 이용한 기본적인 로컬 인증 구현을 완료했습니다. Passport의 Strategy가 어떻게 사용자의 자격 증명을 검증하고, AuthGuard가 이를 트리거하며, 검증된 사용자 정보가 req.user에 주입되는지 그 흐름을 이해하는 것이 중요합니다.

다음 절에서는 실제 웹 애플리케이션에서 널리 사용되는 JWT(JSON Web Tokens) 기반 인증을 구현하는 방법에 대해 더 깊이 있게 다루겠습니다.