NestJS / / 2024. 11. 18. 04:34

NestJS Class-transfomer

문제 상황

기존의 UsersModel 엔티티를 사용하여 사용자 정보를 조회할 때, 비밀번호와 같은 민감한 데이터가 프론트엔드에 노출되고 있었습니다. 프론트엔드에서는 비밀번호 정보가 필요하지 않기 때문에, 이를 숨기기 위한 방법이 필요합니다.

기존 UsersModel 엔티티

import { Column, Entity, OneToMany } from "typeorm";
import { RolesEnum } from "../const/roles.const";
import { PostsModel } from "src/posts/entities/posts.entity";
import { BaseModel } from "src/common/entity/base.entity";
import { IsEmail, IsString, Length } from "class-validator";
import { lengthValidationMessage } from "src/common/validation-message/length-validation.message";
import { stringValidationMessage } from "src/common/validation-message/string-validation.message";
import { emailValidationMessage } from "src/common/validation-message/email-validation.message";

@Entity()
export class UsersModel extends BaseModel {
    @Column({
        length: 20,
        unique: true,
    })
    @IsString({
        message: stringValidationMessage,
    })
    @Length(1, 20, {
        message: lengthValidationMessage,
    })
    nickname: string;

    @Column({
        unique: true,
    })
    @IsString({
        message: stringValidationMessage,
    })
    @IsEmail({}, {
        message: emailValidationMessage,
    })
    email: string;

    @Column()
    @IsString({
        message: stringValidationMessage,
    })
    @Length(3, 8, {
        message: lengthValidationMessage,
    })
    password: string;

    @Column({
        type: 'enum',
        enum: RolesEnum,
        default: RolesEnum.USER,
    })
    role: RolesEnum;

    @OneToMany(() => PostsModel, (post) => post.author)
    posts: PostsModel[];
}

이 엔티티를 기반으로 사용자 정보를 조회하면 비밀번호까지 모두 노출되므로, 이를 숨기기 위해 @Exclude()를 사용합니다.


1. 비밀번호 필드에서 @Exclude() 사용

비밀번호를 제외하기 위해서는 @Exclude() 어노테이션을 추가합니다. @Exclude()class-transformer 라이브러리에서 제공하며, 직렬화 과정에서 특정 필드를 제외할 수 있도록 해줍니다.

import { Exclude } from 'class-transformer';

@Column()
@IsString({
    message: stringValidationMessage,
})
@Length(3, 8, {
    message: lengthValidationMessage,
})
@Exclude()// 이 필드를 직렬화에서 제외
password: string;

설명

  • @Exclude()를 사용하면 JSON 직렬화 과정에서 이 필드가 자동으로 제외됩니다. 따라서 사용자 정보 조회 시, 비밀번호는 프론트엔드로 전달되지 않습니다.
  • Excludeclass-transformer에서 가져와야 하며, 이를 통해 직렬화에서의 제외 처리가 가능해집니다.

2. UsersController에서 직렬화 인터셉터 적용

비밀번호를 실제로 직렬화 과정에서 제외하기 위해서는 ClassSerializerInterceptor를 사용해야 합니다. 이 인터셉터를 사용하면 엔티티에서 @Exclude()와 같은 직렬화 관련 데코레이터가 올바르게 적용됩니다.

UsersController 코드 변경

import { Body, ClassSerializerInterceptor, Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @UseInterceptors(ClassSerializerInterceptor)// 직렬화 인터셉터 사용
  getUsers() {
    return this.usersService.getAllUsers();
  }
}

설명

  • @UseInterceptors(ClassSerializerInterceptor):
    • 이 데코레이터는 응답 직렬화 과정에서 @Exclude() 등의 설정을 반영하도록 해줍니다.
    • getUsers() 메서드에서 사용자 정보를 응답으로 보낼 때, 비밀번호가 포함되지 않도록 보장합니다.

3. 결과 확인

Before (변경 전)

사용자 정보를 조회했을 때 비밀번호가 포함되어 있었습니다:

After (변경 후)

@Exclude()ClassSerializerInterceptor를 사용하면, 사용자 정보 조회 시 비밀번호가 직렬화에서 제외됩니다:

  • 비밀번호가 응답에서 제외되었으며, 보안상 안전하게 처리된 상태입니다.

정리

  1. UsersModel 엔티티에서 password 필드에 @Exclude() 적용:
    • class-transformer 라이브러리에서 제공하는 @Exclude()를 사용하여 비밀번호 필드를 직렬화 과정에서 제외했습니다.
  2. UsersController에서 ClassSerializerInterceptor 적용:
    • @UseInterceptors(ClassSerializerInterceptor)를 사용하여, 응답에서 @Exclude() 설정을 반영할 수 있도록 했습니다.
    • 이를 통해 프론트엔드로 사용자 정보 조회 시 비밀번호가 노출되지 않도록 보장했습니다.

이 방식은 사용자 정보 보호에 매우 유용하며, 특히 비밀번호와 같은 민감한 데이터가 노출되는 것을 방지하는 데 효과적입니다. class-transformerClassSerializerInterceptor를 사용하여 엔티티의 민감한 필드를 관리하는 것은 보안을 위한 중요한 설계 패턴입니다.

그런데 이런 형태로 제외시켜버리면 응답을 받을때도 비밀번호가 제외되기때문에

이 엔티티를 사용하고있는 api에서 문제가 발생하게 됩니다.

비밀번호를 입력을 못받게 되는거죠

그러면 어떻게해야되는가

    /**
     *  Request
     *  frontend -> backend
     *  plain object (JSON) -> class instance (dto)
     * 
     *  Response
     *  backend -> frontend
     *  class instance (dto) -> plain object (JSON)
     * 
     *  toClassOnly -> class instance로 변환될때만
     *  toPlainOnly -> plain object로 변환될때만
     */
    @Exclude({
        toPlainOnly: true,
    })
    password: string;

Exclude에 옵션을 넣어야하는데 우리는 지금 비밀번호를 응답으로 보내려고할때만 비밀번호를 노출을 시키면 안되게때문에 toPlainOnly옵션을 true로 해주면 됩니다.

그럴 일은 없겠지만 만약 요청을 받을때만 제외시켜주고 싶다면 toClassOnly를 트루로 해주시면 됩니다.

@Exclude() 데코레이터와 옵션 설정

기본적으로 @Exclude() 데코레이터를 사용하면 해당 필드는 모든 직렬화(toPlain())와 역직렬화(toClass()) 과정에서 제외됩니다. 즉, 엔티티를 응답으로 변환할 때와 클라이언트에서 데이터를 받는 경우 모두 제외되기 때문에 문제가 생길 수 있습니다.

이를 해결하기 위해서는 옵션을 사용하여 필드가 제외될 타이밍을 조절할 수 있습니다.

옵션 설명

  • toPlainOnly: true:
    • 이 옵션을 사용하면 plain object로 변환될 때만 필드를 제외합니다.
    • 즉, 응답을 생성하는 과정에서만 비밀번호를 제외하는 것이 가능하게 됩니다.
    • 이를 통해 비밀번호를 저장하거나 인증할 때는 사용하지만, 응답 시에는 비밀번호가 포함되지 않도록 제어할 수 있습니다.
  • toClassOnly: true:
    • 이 옵션을 사용하면 클래스 인스턴스로 변환될 때만 필드를 제외합니다.
    • 요청을 받을 때 비밀번호를 제외하고 싶다면 이 옵션을 사용합니다. 하지만 일반적으로 비밀번호는 클라이언트에서 서버로 입력받는 경우에 사용되므로, 이 옵션은 잘 사용되지 않습니다.

적용 흐름 설명

1. 요청 흐름 (Request: JSON -> Class Instance)

  • 클라이언트에서 서버로 데이터를 보낼 때 JSON 형식의 데이터가 클래스 인스턴스로 변환됩니다.
  • password 필드는 요청 데이터로 사용되므로 엔티티 내에서 password가 필요합니다.
  • @Exclude({ toPlainOnly: true })클래스 인스턴스로 변환할 때는 적용되지 않기 때문에 비밀번호를 정상적으로 수신하고 사용할 수 있습니다.

2. 응답 흐름 (Response: Class Instance -> JSON)

  • 서버가 응답 데이터를 보낼 때는 클래스 인스턴스JSON 형식으로 변환하게 됩니다.
  • 이 과정에서 @Exclude({ toPlainOnly: true })에 의해 비밀번호 필드가 제외됩니다.
  • 따라서 응답 객체에서는 비밀번호가 포함되지 않음으로써 보안적인 이점을 제공합니다.

정리

  • @Exclude({ toPlainOnly: true })직렬화 과정(응답 시)에만 비밀번호를 제외하여, 사용자 정보가 프론트엔드로 전달될 때 민감한 데이터가 노출되지 않도록 합니다.
  • 반면에 요청을 받을 때는 비밀번호를 정상적으로 받아 처리할 수 있습니다.

이 설정은 보안적 측면에서 매우 중요하며, 특히 사용자의 비밀번호나 중요한 데이터를 안전하게 다루기 위해 반드시 필요합니다. 이를 통해 보안데이터 처리가 필요한 상황을 모두 충족할 수 있게 됩니다.


문제 상황

위와 같이 설정했지만 다른 API에서도 비밀번호가 노출되는 문제가 발생할 수 있습니다. 예를 들어, 게시물 정보를 조회하면서 작성자(author) 정보가 포함될 때, 비밀번호가 포함될 수 있습니다. 아래와 같은 PostsController가 있다고 가정해봅니다.

@Get()
getPosts() {
    return this.postsService.getAllPosts();
}

async getAllPosts() {
    return this.postsRepository.find({
        relations: ['author'],
    });
}

여기서 getAllPosts 메서드는 게시물(posts) 정보를 가져오며, author 관계를 포함하고 있습니다. 만약 컨트롤러에서 ClassSerializerInterceptor를 사용하지 않으면 author로 연결된 사용자 정보에서 비밀번호가 그대로 포함됩니다.

해결 방법: 모든 컨트롤러에서 직렬화 적용하기

위와 같은 문제를 해결하기 위해서는 ClassSerializerInterceptor를 모든 컨트롤러에 적용해야 합니다. 하지만, 실수로 특정 컨트롤러에 @UseInterceptors()를 추가하지 않으면 비밀번호가 노출될 수 있습니다.

이 문제를 해결하기 위해 앱 전역에 직렬화 인터셉터를 설정할 수 있습니다.

app.module.ts에서 전역 설정하기

app.module.ts 파일로 이동하여 전역 직렬화 인터셉터를 설정합니다.

import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsModel } from './posts/entities/posts.entity';
import { UsersModule } from './users/users.module';
import { UsersModel } from './users/entities/users.entity';
import { AuthModule } from './auth/auth.module';
import { CommonModule } from './common/common.module';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [
    UsersModule,
    PostsModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: '127.0.0.1',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'postgres',
      entities: [PostsModel, UsersModel],
      synchronize: true,
    }),
    AuthModule,
    CommonModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useClass: ClassSerializerInterceptor,
    },
  ],
})
export class AppModule {}

설명

  • 전역 직렬화 인터셉터 설정:
    • APP_INTERCEPTOR를 사용해 앱 전체에 걸쳐 *ClassSerializerInterceptor를 적용했습니다.
    • 이를 통해 각 컨트롤러마다 @UseInterceptors()를 추가하지 않아도 모든 응답에서 비밀번호와 같은 민감한 데이터가 안전하게 처리됩니다.
  • providers: [ AppService, { provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor, }, ],

결과

  • 이제 모든 컨트롤러에서 자동으로 직렬화 인터셉터가 적용되기 때문에, 실수로 인터셉터를 누락해서 발생하는 개인정보 유출 문제를 방지할 수 있습니다.

최종 정리

  1. 비밀번호 필드에서 @Exclude() 사용:
    • @Exclude({ toPlainOnly: true })를 사용하여 응답 시 비밀번호를 제외하고, 요청 시에는 비밀번호가 정상적으로 처리되도록 설정했습니다.
  2. ClassSerializerInterceptor 적용:
    • 개별 컨트롤러에서 직렬화 인터셉터를 적용해 응답 직렬화 시 비밀번호가 제외되도록 했습니다.
  3. 전역 직렬화 설정:
    • app.module.ts에서 전역으로 *ClassSerializerInterceptor를 설정하여, 모든 컨트롤러의 응답에 자동으로 적용되도록 했습니다.
    • 이를 통해 보안 실수를 방지하고, 전역적으로 비밀번호가 노출되지 않도록 보장했습니다.

이런 방식은 보안적으로 중요한 데이터를 보호하는 데 필수적인 설계이며, 비밀번호와 같은 민감한 정보를 안전하게 처리하는 좋은 패턴입니다. 전역으로 설정함으로써 효율성과 안전성을 모두 확보할 수 있습니다.

'NestJS' 카테고리의 다른 글

NestJS Pagination Common  (0) 2024.11.20
NestJS 페이징처리  (0) 2024.11.19
NestJS DTO,Validation  (0) 2024.11.18
NestJS 데코레이터  (0) 2024.11.16
NestJS Guard  (0) 2024.11.16
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유