문제 상황
기존의 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 직렬화 과정에서 이 필드가 자동으로 제외됩니다. 따라서 사용자 정보 조회 시, 비밀번호는 프론트엔드로 전달되지 않습니다.Exclude
는class-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
를 사용하면, 사용자 정보 조회 시 비밀번호가 직렬화에서 제외됩니다:
- 비밀번호가 응답에서 제외되었으며, 보안상 안전하게 처리된 상태입니다.
정리
- UsersModel 엔티티에서
password
필드에@Exclude()
적용:class-transformer
라이브러리에서 제공하는@Exclude()
를 사용하여 비밀번호 필드를 직렬화 과정에서 제외했습니다.
- UsersController에서
ClassSerializerInterceptor
적용:@UseInterceptors(ClassSerializerInterceptor)
를 사용하여, 응답에서@Exclude()
설정을 반영할 수 있도록 했습니다.- 이를 통해 프론트엔드로 사용자 정보 조회 시 비밀번호가 노출되지 않도록 보장했습니다.
이 방식은 사용자 정보 보호에 매우 유용하며, 특히 비밀번호와 같은 민감한 데이터가 노출되는 것을 방지하는 데 효과적입니다. class-transformer
와 ClassSerializerInterceptor
를 사용하여 엔티티의 민감한 필드를 관리하는 것은 보안을 위한 중요한 설계 패턴입니다.
그런데 이런 형태로 제외시켜버리면 응답을 받을때도 비밀번호가 제외되기때문에
이 엔티티를 사용하고있는 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, }, ],
결과
- 이제 모든 컨트롤러에서 자동으로 직렬화 인터셉터가 적용되기 때문에, 실수로 인터셉터를 누락해서 발생하는 개인정보 유출 문제를 방지할 수 있습니다.
최종 정리
- 비밀번호 필드에서
@Exclude()
사용:@Exclude({ toPlainOnly: true })
를 사용하여 응답 시 비밀번호를 제외하고, 요청 시에는 비밀번호가 정상적으로 처리되도록 설정했습니다.
ClassSerializerInterceptor
적용:- 개별 컨트롤러에서 직렬화 인터셉터를 적용해 응답 직렬화 시 비밀번호가 제외되도록 했습니다.
- 전역 직렬화 설정:
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 |