본문 바로가기

Project/Nest.js

[번역] Passport란 무엇인가?

passport 관한 내용을 번역과 동시에 제 생각을 넣은 글입니다. 더 정확한 사실을 알고 싶다면 해당 링크를 방문해 주세요. 번역 시작하겠습니다.

오늘은 토요일 맞아.

passport는 node.js에서 인증을 처리하기 위한 미들웨어입니다. 이 미들웨어는 다양한 전략(strategy)를 이용해 인증을 구현할 수 있습니다. 그 중 passport-local은 사용자가 로컬 데이터베이스에 저장된 username, password를 사용해 인증을 처리하는 전략입니다.

passport-local의 전략은 기본적으로 LocalStrategy 클래스를 확장해 생성됩니다. 이 클래스의 생성자는 기본 설정으로 동작하게 되어있지만, 옵션 객체를 전달해 동작 방식을 커스터마이즈할 수 있습니다. 예를 들어서 username이 아닌 email을 사용해 커스터마이즈도 가능합니다. 이제 아래에 passport를 이용한 전략을 구현해보겠습니다.

Passport

passport를 구현하기전에 우리는 passport를 하나의 미니 프레임워크로 생각하는 것이 좋습니다. 왜 프레임워크라고 말씀드렸냐면, 사용자가 구현하는 전략에 따라서 몇가지 기본 단계로 인증 프로세스를 추상화할 수 있기 때문입니다. 이처럼 passport는 프레임워크와 같습니다.

그리고, 사용자가 지정한 매개변수와 사용자가 지정한 코드를 콜백함수 형태로 제공해 구성합니다. 또한, passport는 적절한 때에 콜백함수를 호출해 실행을 원활하게 시킬 수 있습니다.

또 다른 특징으로는, @nestjs/passport 모듈은 Nest 스타일 패키지로 프레임워크를 감싸고 있습니다. 그래서 Nest 어플리케이션으로 쉽게 통합될 수 있습니다. 우리는 @nestjs/passport를 사용할 것이지만 일단 바닐라 passport의 작동 방식을 알아보도록 하겠습니다.

바닐라 passport에서는 다음 두가지를 제공해 전략을 구성하고 있습니다. 첫번째는 해당 전략에서 제공되는 특정 옵션이 존재합니다. 예를들어서, JWT 전략에서 토큰을 체크할 수 있는 방법을 제공할 수 있습니다. 이러한 설정은 다른 전략에서 제공되지 않습니다. 이렇듯, 각 전략은 그 전략의 특성에 맞는 고유한 옵션 세트를 가지고 있습니다. 

간단히 설명하자면, passport-local 전략에서는 usernameField와 passwordField와 같은 필드를 사용자 정의할 수 있는 옵션이 있으며, passport-jwt에서는 JWT 토큰을 처리하기 위한 옵션이 주어집니다.

두번째는 validate callback 함수는 우리가 사용자 계정을 관리하는 곳과 상호작용 하는 방법을 passport에 알립니다. 여기에서 사용자가 존재하는가? 그리고 해당 사용자의 자격이 유효한지를 확인할 수 있습니다.

passport 라이브러리는 이 콜백이 유효성 확인에 성공하면 전체 사용자를 반환합니다. 하지만 실패하면 null을 반환하게 됩니다. 예를 들어서, 사용자를 찾을 수 없거나 passport-local의 경우 암호가 일치하지 않거나와 같습니다.

@nestjs/passport를 사용하면 passport Strategy 클래스를 확장해 passport 전략을 구성합니다. 이처럼 서브 클래스를 만들어 super() 메서드를 호출해 strategy 옵션을 통과하고 옵션 개체를 통과하게 됩니다. 그리고 서브 클래스에서 validate() 메서드를 구현해 검증 콜백을 제공합니다. 

더보기

서브 클래스란(sub class)?

프로그래밍에서 상속을 통해 만들어진 클래스를 의미합니다. 즉, 다른 클래스를 기반으로 확장(extends)하여 새로운 기능이나 동작을 추가한 클래스를 의미합니다. passport 라이브러리는 모드 기본적으로 Strategy 클래스를 상속받아 구현합니다.

passport를 구현하는 과정

원래는 TypeORM, Sequelize, Mongoose등을 사용해서 DB를 사용해 구성하지만 현재 우리는 단순한 애플리케이션을 보여줄 예정입니다. 그래서 메모리에 데이터를 기록하고 하드코딩해 DB를 만들어 구현해보도록 하겠습니다.

Dir : users/users.service.ts

import { Injectable } from '@nestjs/common';

export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: 'john',
      password: 'changeme',
    },
    {
      userId: 2,
      username: 'maria',
      password: 'guess',
    },
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }
}

우리는 AuthService에서 UserService의 함수가 필요하기 때문에, UserModule에서 exports 배열에 UserService를 추가해야합니다. 그래서 @Injectable이 추가되어 있습니다. 코드는 아래와 같습니다.

users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

AuthService는 사용자를 검색하고 비밀번호를 검증하는 역할을 갖고있습니다. 우리는 사용자 검색과 비밀번호 검증을 위해서 validateUser() 메소드를 만들어야합니다. 아래 코드에서는 ES6 스프레드 연산자를 사용해 패스워드를 제거하고 받아온 데이터를 반환합니다. 

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;
  }
}

그리고, 우리는 우리의 AuthModule도 업데이트를 해줘야합니다. import에 UserModule을 추가해줍니다.

auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
})
export class AuthModule {}

passport-local 

이제는 passport-local을 구현해볼겁니다. auth 폴더에서 local.strategy.ts라는 파일을 생성하고 아래 코드를 그대로 작성해봅시다.

우리는 모든 Passport 전략에 대해 앞에서 설명한 레시피를 따르게 되는데, Passport local 사용 사례의 경우 구성 옵션이 없기 때문에 생성자는 옵션없이 super()라고만 부릅니다.

auth/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()
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;
  }
}

일단 기본적으로 passport-local 전략은 request body에서 username, password 프로퍼티를 받으려고 대기하고 있습니다. 물론 request body에 username과 password를 프로퍼티로 지정을 해둬야 겠지요?

그리고 우리는 validate() 메소드를 구현해두었습니다. 각 전략에 대해 passport는 적절한 전략별 파라미터(username, password)를 사용해 검증이 가능합니다. 우리가 구현하고있는 local 전략의 경우 Passport validate() 메서드에 validate의 검증을 대기하고있습니다.

이와같이 passport 전략에서 validate()를 사용한 인증 방법은 자격 증명이 표시되는 방법(방금 username, password로 인증하듯)에 대한 세부 사항만 변경되면서 유사한 패턴을 따르게 됩니다.

또다른 방법으로 JWT 전략은 요구사항들에 의존하는데 우리는 우리의 User DB 또는 삭제된 토큰 리스트에서 매치해 정보가 맞는지 확인합니다.

따라서 전략별 검증을 sub class에서 구현하는 패턴은 일관되고 우아하다는 것입니다. 그리고 확장성도 뛰어나게 됩니다.

방금 정의한 여권 기능을 사용하도록 AuthModule을 구성해야 합니다. 코드는 아래와 같습니다.

auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

다음에 이어서 설명

'Project > Nest.js' 카테고리의 다른 글

[번역] Pipes란 무엇인가?  (0) 2024.08.31
[번역] Guard란 무엇인가?  (0) 2024.08.29