들어가며

이번 게시글에서는 Nest.js 에서 로그인 기능을 구현하는 방법에 대해 알아볼 예정입니다.

Nest.js 의 공식 문서를 참고하여 이 게시글을 작성할 예정입니다.

공식 문서 : https://docs.nestjs.com/security/authentication  

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com


Nest.js 공식 문서에서는 Passport 라이브러리 사용을 권장하고 있습니다.
로그인 기능 구현 방식으로 passport-local 방식과 passport-jwt 방식이 있는데, 이번 게시글에서는 passport-local 방식을 사용해 로그인 기능을 구현해 볼 예정입니다.

설치

아래와 같이 로그인 기능 구현을 위해 필요한 모듈을 설치해줘야 합니다.

npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local

모듈 설치가 완료되었다면 nest-cli 로 auth module과 auth service를 만들어 줍니다.

# module 생성
nest g module auth

# service 생성
nest g service auth

다음으로 auth 폴더 안에 local-auth.guard.ts 와 local-auth.strategy.ts, local-serializer.ts 파일을 생성합니다.

local-auth.guard.ts

import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

local-auth.strategy.ts

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({ usernameField: 'email', passwordField: 'password' });
  }

  async validate(email: string, password: string, done: CallableFunction) {
    const user = await this.authService.validateUser(email, password);
    if (!user) throw new UnauthorizedException(); // 401
    return done(null, user);
  }
}

local-auth.strategy.ts 파일에서 중요한 부분은 super(); 를 통해 부모(PassportStrategy) 속성을 변경해 HTTP 요청 시 각각 usernameField, passwordField 키 값이 아닌 email, password 키 값으로 요청이 가능하게 변경해줘야 합니다.
그리고 반드시 validate 메서드를 구현해줘야 합니다.
validate 메서드에서는 리퀘스트 바디로 들어온 email, password 값을 가지고 사용자 존재 여부를 확인합니다.

사용자가 존재하지 않는 경우 401 Unauthorized 예외가 발생합니다.

local.serializer.ts

import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Users } from 'src/entities/Users';
import { Repository } from 'typeorm';

@Injectable()
export class LocalSerializer extends PassportSerializer {
  constructor(
    @InjectRepository(Users) private usersRepository: Repository<Users>,
  ) {
    super();
  }

  // 사용자 정보(간단하게 userId) 를 Session에 저장
  serializeUser(user: Users, done: CallableFunction) {
    done(null, user.id);
  }

  // 인증 후, 페이지 접근시 마다 사용자 정보를 Session에서 읽어옴. (req.user)
  async deserializeUser(userId: string, done: CallableFunction) {
    return await this.usersRepository
      .findOneOrFail({
        where: { id: +userId },
        select: ['id', 'email', 'nickname'],
        relations: ['Workspaces'],
      })
      .then((user) => {
        console.log('user', user);
        done(null, user); // req.user
      })
      .catch((error) => done(error));
  }
}

serializeUser 메서드를 이용하여 사용자 정보를 Session 저장할 수 있습니다.

serializeUser 메서드에서는 user, done 인자를 이용해서 session  저장할 정보를 done(null, user.id) 과 같이 두번째 인자로 넘기면 됩니. 이때 user 인자 넘어오는 정보는 앞의 local-auth.strategy.ts 파일에 있는 LocalStrategy 객체의 validate 함수에서 done(null, user) 의해 리턴된 값이 넘어옵니다.

 

다음으로, 우리 어플리케이션에서 사용자 정보를 필요로 하는 API 에 접근할때, 로그인이 되어 있을 경우 deserilizeUser 메서드가 호출됩니. deserializeUser 메서드에서는 session  저장된 값을 이용해서 사용자 정보를 찾은 후 done(null, user) 를 이용해서 리턴합니다. 이렇게 리턴된 user 는 HTTP Request  “req.user” 의 값으로 추가됩니다.

auth.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/Users';
import { AuthService } from './auth.service';
import { LocalSerializer } from './local.serializer';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [
    PassportModule.register({ session: true }),
    TypeOrmModule.forFeature([Users]),
  ],
  providers: [AuthService, LocalStrategy, LocalSerializer],
})
export class AuthModule {}

우리가 생성한 LocalStrategy, LocalSerializer 와 PassportModule, Users 엔티티를 등록합니다.

세션을 사용하기 위해서는 PassportModule 등록 시 register 메서드의 인자 값으로 session: true 로 설정된 옵션 객체를 넣어줘야 합니다.

auth.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import bcrypt from 'bcrypt';
import { Users } from 'src/entities/Users';
import { Repository } from 'typeorm';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(Users)
    private readonly usersRepository: Repository<Users>,
  ) {}

  /**
   * @author Ron
   * @description 단일 사용자 조회
   *
   * @param email 사용자 이메일
   * @param password 사용자 비밀번호
   * @returns Users
   */
  async validateUser(email: string, password: string): Promise<any> {
    const user = await this.usersRepository.findOne({
      select: ['id', 'email', 'password', 'nickname'],
      where: { email },
    });
    if (!user) return null;

    // 사용자가 요청한 비밀번호와 데이터베이스에서 조회한 비밀번호 일치여부 검사
    const result = await bcrypt.compare(password, user.password);
    if (result) {
      // userWithoutPassword: 비밀번호를 제외한 나머지 속성 집합들
      const { password, ...userWithoutPassword } = user;
      // 비밀번호를 제외하고 사용자 정보 반환
      return userWithoutPassword;
    }
    return null;
  }
}

아까 생성한 auth.service.ts 파일에는 실제 데이터베이스에 접근해서 사용자를 조회하고 로그인 비즈니스 로직을 구현합니다. 

 

인증 기능을 구현할 때 사용 가능할 수 있도록 Nest.js 에서는 Guards 라는 기능을 제공합니다.

users.controller.ts

@UseGuards(LocalAuthGuard)
@Post('/auth/login')
async logIn(@User() user) {
  return user;
}

 

클라이언트의 로그인 요청에 따라 어플리케이션에서 진행되는 흐름은 다음과 같은 순서로 진행됩니다.

LocalStrategy -> AuthService -> LoginSerializer -> Login Route -> Client

'라이브러리 & 프레임워크 > NestJS' 카테고리의 다른 글

[Nest] WebSocket 서버 구성하기(feat. Socket.io)  (0) 2022.12.12
Exception filter  (0) 2022.11.28
Validation  (0) 2022.11.28
Mapped Types  (0) 2022.11.28
[TypeORM] 마이그레이션  (0) 2022.11.27
복사했습니다!