NestJS에서의 인증 토큰 시스템

해싱(Hashing)과 기본 토큰 개념

해싱의 중요성

인증 시스템의 기본은 안전한 비밀번호 저장입니다. 해싱은 원본 데이터를 고정 길이의 문자열로 변환하는 일방향 함수로, 원본으로 되돌릴 수 없습니다. NestJS에서는 bcrypt와 같은 라이브러리를 사용하여 비밀번호를 해싱합니다.

import * as bcrypt from 'bcrypt';
 
// 비밀번호 해싱
const hashPassword = async (password: string): Promise<string> => {
  const salt = await bcrypt.genSalt(10);
  return bcrypt.hash(password, salt);
};
 
// 비밀번호 검증
const validatePassword = async (
  plainPassword: string, 
  hashedPassword: string
): Promise<boolean> => {
  return bcrypt.compare(plainPassword, hashedPassword);
};

토큰 기본 개념

토큰은 인증된 사용자를 식별하는 암호화된 문자열입니다. 서버는 인증 성공 시 토큰을 발급하고, 클라이언트는 이후 요청에 이 토큰을 포함시켜 자신을 인증합니다.

Passport와 인증 전략

NestJS는 Passport 라이브러리를 통해 다양한 인증 전략을 지원합니다.

Local Strategy (로컬 전략)

사용자 이름과 비밀번호를 사용한 전통적인 인증 방식입니다.

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

JWT Strategy (JWT 전략)

토큰 기반 인증을 위한 전략으로, 클라이언트가 제공한 JWT 토큰을 검증합니다.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

JWT (JSON Web Token)

JWT는 당사자 간에 정보를 안전하게 전송하기 위한 개방형 표준입니다.

JWT 구조

JWT는 헤더(Header), 페이로드(Payload), 서명(Signature)의 세 부분으로 구성됩니다:

  • 헤더: 토큰 유형과 사용된 알고리즘을 지정
  • 페이로드: 클레임(사용자 ID, 역할 등)을 포함
  • 서명: 토큰의 무결성을 검증

NestJS에서 JWT 구현

import { JwtService } from '@nestjs/jwt';
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
 
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
 
  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
 
  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

모듈 설정

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { jwtConstants } from './constants';
 
@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '60m' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

토큰 관리

토큰 발급

사용자 인증 후 JWT 토큰을 발급합니다:

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}
 
  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}

토큰 검증

JWT 가드를 사용하여 보호된 라우트에 대한 액세스를 제어합니다:

@Controller('profile')
export class ProfileController {
  @UseGuards(JwtAuthGuard)
  @Get()
  getProfile(@Request() req) {
    return req.user;
  }
}

토큰 갱신

토큰 만료 시 갱신 메커니즘을 구현합니다:

@Injectable()
export class AuthService {
  // ... 기존 코드
 
  async refreshToken(refreshToken: string) {
    try {
      // 리프레시 토큰 검증
      const payload = this.jwtService.verify(refreshToken, {
        secret: jwtConstants.refreshSecret,
      });
      
      // 새 액세스 토큰 발급
      const newAccessToken = this.jwtService.sign(
        { username: payload.username, sub: payload.sub },
        { expiresIn: '1h' }
      );
      
      return {
        access_token: newAccessToken,
      };
    } catch (e) {
      throw new UnauthorizedException('Invalid refresh token');
    }
  }
}

토큰 만료 및 블랙리스트

보안 강화를 위해 로그아웃 시 토큰을 블랙리스트에 추가할 수 있습니다:

@Injectable()
export class TokenBlacklistService {
  private readonly blacklist: Set<string> = new Set();
 
  addToBlacklist(token: string): void {
    this.blacklist.add(token);
  }
 
  isBlacklisted(token: string): boolean {
    return this.blacklist.has(token);
  }
}

보안 고려사항

  1. 환경 변수 사용: JWT 비밀 키는 환경 변수로 관리
  2. 토큰 만료 시간: 짧은 만료 시간 설정 (액세스 토큰 1시간, 리프레시 토큰 2주)
  3. HTTPS: 모든 API 통신은 HTTPS로 암호화
  4. XSS 및 CSRF 방어: HttpOnly 쿠키 사용 고려
  5. 토큰 저장소: 클라이언트 측에서는 안전한 저장소(HttpOnly 쿠키, 로컬 스토리지 대신)에 저장

이러한 인증 및 토큰 관리 시스템은 안전하고 확장 가능한 NestJS 애플리케이션 구축의 기반이 됩니다.

더 상세한 정보나 특정 부분에 대해 더 알고 싶으시다면 말씀해 주세요!