Published on

NestJS로 구축하는 사용자 인증 시스템-회원가입 및 로그인 구현

Authors
  • Name
    황도연
    Twitter

NestJS로 구축하는 사용자 인증 시스템: 회원가입 및 로그인 구현

GitHub Repository: NestJS backend login

필요한 기술 스택

  • NestJS: Node.js 서버 사이드 프레임워크
  • TypeORM: 데이터베이스 작업을 위한 ORM
  • JWT(JSON Web Tokens): 사용자 인증 및 권한 부여
  • bcrypt: 비밀번호 암호화

회원가입 기능 구현

1단계: 사용자 엔티티 생성

User 엔티티는 사용자 정보를 저장합니다.

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import * as bcrypt from 'bcrypt'

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ unique: true, type: 'text' })
  name: string

  @Column({ unique: true, type: 'text' })
  email: string

  @Column()
  password: string

  @Column()
  company: string

  @Column({ nullable: true })
  job: string

  @Column({ unique: true })
  nickname: string

  @CreateDateColumn()
  createdAt: Date

  @UpdateDateColumn()
  updatedAt: Date

  // isAdmin: boolean
  @Column({ default: false })
  isAdmin: boolean

  async validatePassword(password: string): Promise<boolean> {
    return bcrypt.compare(password, this.password)
  }
}

DTO

LoginDto

import { ApiProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'

export class LoginDto {
  @ApiProperty({
    description: 'email',
    default: 'test2@gmail.com',
    required: true,
  })
  @IsEmail()
  email!: string

  @ApiProperty({
    description: 'password',
    default: 'password',
    required: true,
  })
  @IsString()
  password!: string
}

SignUpDto

import { ApiProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'

export class SignupDto {
  @ApiProperty({
    description: 'name',
    default: 'TESTER',
    required: true,
  })
  @IsString()
  name!: string

  @ApiProperty({
    description: 'email',
    default: 'test2@gmail.com',
    required: true,
  })
  @IsEmail()
  email!: string

  @ApiProperty({
    description: 'company',
    default: 'company',
    required: true,
  })
  @IsString()
  company!: string

  @ApiProperty({
    description: 'job',
    default: 'job',
    required: true,
  })
  @IsString()
  job: string

  @ApiProperty({
    description: 'nickname',
    default: 'nickname',
    required: true,
  })
  @IsString()
  nickname: string

  @ApiProperty({
    description: 'password',
    default: 'password',
    required: true,
  })
  @IsString()
  password!: string
}

2단계: 회원가입 로직

signup 메소드는 회원가입을 처리합니다.

public async signup(payload: SignupDto): Promise<Token> {
  const { name, email, company, job, nickname, password } = payload;

  const hashedPassword = await generatePassword(password);

  const token: Token = {
    accessToken: '',
    refreshToken: '',
  };

  const addUserValue = this.userRepository.create({
    name,
    email,
    company,
    job,
    nickname,
    password: hashedPassword,
  });
  const addUserResult = await this.userRepository.save(addUserValue);

  const refreshToken = generateRandom(32);
  const expiredAt = new Date(Date.now() + 3600 * 1000 * 24);
  const addRefreshTokenValue = this.refreshTokenRepository.create({
    userId: addUserResult.id,
    refreshToken: refreshToken,
    expiredAt: expiredAt,
    isBlocked: false,
  });
  await this.refreshTokenRepository.addRefreshToken(addRefreshTokenValue);

  token.tokenPayload = {
    id: addUserResult.id,
    email: addUserResult.email,
  };
  token.accessToken = generateToken(token.tokenPayload);
  token.refreshToken = refreshToken;

  return token;
}

3단계: JWT 생성

JWT 토큰 생성 예시입니다.

token.accessToken = generateToken(token.tokenPayload)
token.refreshToken = generateRandom(32)

로그인 기능 구현

1단계: 사용자 검증

login 메소드에서는 사용자를 검증합니다.

public async login(payload: LoginDto): Promise<Token> {
  const { email, password } = payload;

  const user: User | undefined = await this.userRepository.findOne({
    where: { email },
  });

  const comparePassword = await validatePassword(password, user.password);
}

2단계: JWT 토큰 생성

로그인 성공 시 JWT 토큰을 생성합니다.

token.accessToken = generateToken({ id: user.id, email: user.email })
token.refreshToken = generateRandom(32)

3단계: 로그인 코드와 트랜잭션 관리

트랜잭션 관리 예시와 로그인 코드입니다.

public async login(payload: LoginDto): Promise<Token> {
  const { email, password } = payload;

  const user: User | undefined = await this.userRepository.findOne({
    where: { email },
  });
  if (!user) {
    throw new Error('USER_NOT_FOUND');
  }

  const comparePassword = await validatePassword(password, user.password);
  if (!comparePassword) {
    throw new Error('USER_NOT_FOUND');
  }

  const token: Token = {
    accessToken: '',
    refreshToken: '',
  };

  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    const refreshToken = generateRandom(32);
    const expiredAt = new Date(Date.now() + 3600 * 1000 * 24);
    const addRefreshTokenValue = this.refreshTokenRepository.create({
      userId: user.id,
      refreshToken,
      isBlocked: false,
      expiredAt,
    });
    const addRefreshToken =
      await this.refreshTokenRepository.addRefreshToken(addRefreshTokenValue);
    if (!addRefreshToken) {
      throw new Error('AFFECTED_ROWS_ERROR');
    }

    token.tokenPayload = {
      id: user.id,
      email: user.email,
      isAdmin: user.isAdmin,
    };
    token.accessToken = generateToken(token.tokenPayload);
    token.refreshToken = refreshToken;

    await queryRunner.commitTransaction();
  } catch (err) {
    await queryRunner.rollbackTransaction();
    throw err;
  } finally {
    await queryRunner.release();
  }

  return token;
}

리프레시 토큰 관리: Redis 사용의 이점

리프레시 토큰을 데이터베이스에 저장하는 대신 Redis와 같은 인-메모리 데이터 저장소를 사용하는 것이 좋습니다.

  1. 성능 향상 : Redis는 인-메모리 데이터 저장소로, 빠른 읽기 및 쓰기 작업을 제공합니다. 이는 특히 대규모 트래픽과 높은 요청률을 처리할 때 유용합니다.
  2. 확장성 : Redis는 확장성이 뛰어나며, 클러스터링을 통해 더 큰 데이터 셋과 높은 처리량을 쉽게 관리할 수 있습니다.
  3. 자동 만료 처리 : Redis는 데이터에 대한 만료 시간을 설정할 수 있어, 리프레시 토큰과 같은 임시 데이터를 자동으로 관리할 수 있습니다. 이는 토큰이 만료되면 자동으로 삭제되어 시스템의 안정성을 높이는 데 도움이 됩니다.
  4. 보안 강화 : 리프레시 토큰은 사용자의 세션을 재생성할 수 있는 중요한 정보입니다. Redis와 같은 별도의 시스템에 저장함으로써, 데이터베이스가 해킹당할 경우의 리스크를 줄일 수 있습니다.