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) 인증 구현
Passport 인증 구현은 strategy 선택, validate 반환값, guard 적용 위치, 실패 응답 기준으로 읽습니다.
가장 기본적인 인증 방식인 로컬 인증(아이디와 비밀번호 사용)을 NestJS와 Passport를 사용하여 구현해 보겠습니다.
단계 1: 필요한 패키지 설치npm install @nestjs/passport passport passport-local @types/passport-local
npm install --save-dev @types/passport # passport 타입 정의도 설치사용자 정보를 관리하는 UsersModule, UsersService가 이미 있다고 가정합니다. UsersService는 사용자 정보(여기서는 간단히 username과 password)를 데이터베이스(또는 메모리)에서 조회하는 기능을 포함합니다.
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;
}
}LocalStrategy)
Passport는 인증 로직을 전략으로 캡슐화합니다. 로컬 인증을 위한 LocalStrategy를 생성합니다.
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에 주입됩니다.
}
}AuthModule) 생성
인증 관련 로직을 하나의 모듈로 묶습니다.
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에 통합하는 모듈.
AuthService) 작성
실제 로그인 로직을 처리하는 서비스를 작성합니다. 여기서는 validateUser를 호출하고, 성공 시 JWT 토큰 등을 발행하는 역할을 할 수 있습니다. 지금은 단순히 사용자 정보를 반환합니다.
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;
}
}AuthController) 작성
클라이언트의 로그인 요청을 처리하는 엔드포인트를 만듭니다. @UseGuards(AuthGuard('local'))를 사용하여 Passport의 Local 전략을 활성화합니다.
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);
}
}AuthModule 임포트
AppModule에 AuthModule을 임포트하여 애플리케이션이 인증 기능을 사용할 수 있도록 합니다.
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 {}다음 흐름도는 Local 인증 요청이 req.user와 로그인 응답으로 이어지는 지점을 압축합니다.
테스트해보기
애플리케이션을 실행합니다. npm run start:dev
Postman이나 curl을 사용하여 http://localhost:3000/auth/login으로 POST 요청을 보냅니다.
- Body는
x-www-form-urlencoded또는raw (JSON)형태로username과password를 포함해야 합니다.{ "username": "testuser", "password": "password123" } x-www-form-urlencoded를 사용하는 경우:username=testuserpassword=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) 기반 인증을 구현하는 방법에 대해 더 깊이 있게 다루겠습니다.