Nest.js 인증 기능에 대해 알아보기

shooting star·2024년 5월 9일
0
post-thumbnail

들어가며

인증(Authentication)은 대부분의 애플리케이션에서 필수적인 부분입니다. Passport는 커뮤니티에서 잘 알려져 있고 많은 프로덕션 애플리케이션에서 성공적으로 사용되는 가장 인기 있는 Node.js 인증 라이브러리입니다. @nestjs/passport 모듈을 사용하여 이 라이브러리를 Nest.js 애플리케이션과 통합하는 것은 간단합니다.

개발 순서

  1. 유저 기능: 회원가입, 로그인, 권한 부여
  2. 암호화: 비밀번호 암호화
  3. 로그인:
    • JWT: 인증 토큰 생성
    • Passport: 인증을 위한 Passport 적용
    • 데이터 직렬화: 객체 직렬화를 통한 데이터 커스텀
  4. 권한: Guards

비밀번호 암호화 (Bcrypt)

비밀번호는 반드시 해싱하여 저장해야 합니다. 이를 위해 Bcrypt를 사용합니다.

Bcrypt 소개

Bcrypt는 비밀번호 해싱에 사용되는 오픈소스 알고리즘으로, 단방향 암호화 알고리즘입니다. 암호화 시 고유한 Salt를 생성하여 해시에 포함함으로써 레인보우 테이블 공격을 방어합니다.

Bcrypt 설치

npm install bcrypt

비밀번호 암호화 예시

import * as bcrypt from 'bcrypt';

export class AuthService {
  async hashPassword(password: string): Promise<string> {
    const salt = await bcrypt.genSalt();
    return bcrypt.hash(password, salt);
  }

  async comparePasswords(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}

인증 토큰 생성하기 (JWT)

JWT(Json Web Token)는 Json 형식의 토큰에 대한 표준 규격으로, 인증 및 권한 정보를 주고받기 위해 사용됩니다. Base64로 인코딩된 형식이며, 주로 클라이언트에서 Authorization HTTP 헤더에 Bearer <토큰>으로 설정하여 서버로 전송합니다.

JWT 구조

  1. HEADER:
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  2. PAYLOAD:
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
  3. SIGNATURE:
    HMACSHA256(
      base64UrlEncode(header) + "." + base64UrlEncode(payload),
      secret
    )
  4. JWT 토큰 예시:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT 필드 설명

  • sub: 인증 주체(subject)
  • iss: 토큰 발급처(issuer)
  • iat: 발급 시각(issued at)
  • exp: 만료 시각(expiration time)

JWT 장단점

  • 장점:
    • 토큰에 사용자 정보가 포함되어 있어, 서버에서는 토큰만 검증하면 됨.
    • 다른 도메인의 API를 쉽게 사용 가능.
  • 단점:
    • 비교적 크기가 커 많은 양의 데이터를 전송하는 경우 네트워크 부하가 있을 수 있음.
    • 한 번 발급된 JWT는 만료 시간이 될 때까지 계속 유효함. 강제 만료가 어려움.

JWT 설정 및 적용

설치:

npm install @nestjs/jwt passport-jwt

auth.module.ts 설정:

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'secretKey',
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

auth.service.ts:

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

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

jwt.strategy.ts:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'secretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

Passport를 적용한 로그인 구현

Passport는 다양한 인증 전략을 지원하는 인증 미들웨어로, Local, Google, Facebook, Apple, Kakao, Naver, Twitter 등 무수히 많은 인증 전략을 지원합니다.

Passport 설치

npm install @nestjs/passport passport passport-local

로컬 전략 구현

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

auth.service.ts에 유저 검증 추가:

@Injectable()
export class AuthService {
  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;
  }
}

로그인 및 가드 적용

auth.controller.ts:

import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './local-auth.guard';
import { AuthService } from './auth.service';

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

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}

local-auth.guard.ts:

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

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

객체 직렬화를 통한 데이터 커스텀

객체 직렬화는 데이터를 원하는 형태로 변환하는 작업입니다. Nest.js에서는 ClassSerializerInterceptor를 사용하여 자동으로 데이터를 직렬화합니다.

직렬화 예시

user.entity.ts:

import { Exclude }

 from 'class-transformer';

export class User {
  id: number;
  username: string;

  @Exclude()
  password: string;
}

user.controller.ts:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ClassSerializerInterceptor } from '@nestjs/common';

@Controller('user')
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
  @Get()
  getProfile() {
    return new User();
  }
}

결론

인증 기능을 도입함으로써 애플리케이션의 보안 수준이 향상되었습니다. 이 블로그를 통해 Nest.js에서 PassportJWT를 활용하여 인증 기능을 구축하는 기본 방법을 배웠습니다.

1개의 댓글

comment-user-thumbnail
2025년 1월 27일

좋은 글 감사합니다. 혹시 PassportStrategy 버젼이 어떻게 되나요?
JwtStrategy의 생성자의 super의 인자가 올바르지 않다고 떠서요 ㅠㅠ

답글 달기