안녕하세요, 이중혁입니다

배우고 경험한 기술들을 하나씩 정리하는 공간

인증 & 보안 시스템

JWT 기반 인증부터 SQL 인젝션 방지까지
다양한 보안 기능을 학습하고 구현했습니다.

인증 시스템

JWT, API Key, OAuth

권한 관리

RBAC, 역할 기반

Rate Limiting

요청 제한, DDoS 방지

데이터 보안

SQL 인젝션, 입력 검증

JWT 기반 인증 시스템

구현 내용

회원가입 프로세스

  • • 이메일 중복 확인
  • • 패스워드 복잡성 검증
  • bcrypt.hash(password, 10) 암호화
  • • 사용자 정보 생성
  • • JWT 토큰 발급

로그인 프로세스

  • • 이메일/패스워드 검증
  • bcrypt.compare(password, hashedPassword) 확인
  • • 사용자 정보 조회
  • • JWT Payload 생성
  • • Access Token과 Refresh Token 발급

토큰 구조

  • • Access Token: 1시간 만료
  • • Refresh Token: 15일 만료
  • • 별도 시크릿 키 사용
  • • 데이터베이스에 Refresh Token 저장

비밀번호 보안

  • bcrypt 해시 알고리즘 사용
  • Salt Round 10 적용
  • • 원본 비밀번호는 저장하지 않음
  • • 해시된 비밀번호만 DB에 저장

API 테스트

테스트 방법

  • 이메일: 아무 이메일 형식으로 넣어주세요
  • 패스워드: 소문자, 대문자, 숫자, 특수문자, 10글자 이상

테스트

@ApiTags('auth')
@Controller('auth')
export class AuthController {
  @Post('register')
  @ApiResponseEntity({
    type: AuthSignUserOutDto,
    summary: '회원가입 및 로그인',
  })
  async register(
    @Body() authSignUserInDto: AuthSignUserInDto,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const authSignUserOutDto =
      await this.authService.register(authSignUserInDto);

    return ResponseEntity.ok().body(authSignUserOutDto);
  }
  
  @Get('jwt-validator')
  @ApiResponseEntity({ type: UserDto, summary: 'jwt 유효성 검증' })
  @ApiBearerAuth('jwt')
  @UseGuards(JwtAuthGuard)
  async jwtValidator(@CurrentUser() user: JwtPayload): Promise<ResponseEntity<JwtPayload>> {
    return ResponseEntity.ok().body(user);
  }

  @Post('refresh')
  @ApiResponseEntity({ type: AuthSignUserOutDto, summary: '토큰 갱신' })
  @ApiBearerAuth('jwt-refresh')
  @UseGuards(JwtRefreshAuthGuard)
  async refreshToken(
    @CurrentUser() user: User,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const authSignUserOutDto = await this.authService.refreshToken(user);

    return ResponseEntity.ok().body(authSignUserOutDto);
  }
}

Session 기반 인증 시스템

구현 내용

세션 생성 프로세스

  • • UUID 기반 Session ID 생성
  • • Redis Multi를 사용한 트랜잭션 처리
  • • 사용자 정보 (userId, gameDbId, nid) 저장
  • • 환경별 TTL 설정 (TEST: 무제한, PROD: 설정값)
  • • ContextProvider에 세션 정보 저장

Redis 트랜잭션

  • Multi 명령: 원자적 세션 생성
  • DB 동기화: TypeORM과 Redis 일관성
  • TTL 설정: 자동 만료 처리
  • 성능 최적화: 파이프라인 명령 처리

세션 구조

  • • Session ID: UUID로 생성된 고유 식별자
  • • User ID: 사용자 식별자
  • • Game DB ID: 게임 데이터베이스 식별자
  • • NID: 네트워크 식별자

보안 강화

  • Redis 기반: 서버 세션 저장
  • TTL 관리: 자동 만료 처리
  • 트랜잭션: 원자적 세션 생성
  • 상태 관리: 서버에서 세션 상태 추적

API 테스트

테스트 방법

  • 이메일: 아무 이메일 형식으로 넣어주세요
  • 패스워드: 소문자, 대문자, 숫자, 특수문자, 10글자 이상

테스트

@Controller('auth')
export class AuthController {
  @Post('register')
  @ApiResponseEntity({
    type: AuthSignUserOutDto,
    summary: '회원가입 및 로그인',
  })
  async register(
    @Body() authSignUserInDto: AuthSignUserInDto,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const result = await this.authService.register(authSignUserInDto);
    return ResponseEntity.ok().body(result);
  }

  /**
   * Session 인증
   */
  @Auth()
  @Post('session')
  @ApiResponseEntity({ type: Session, summary: 'Session 유효성 검증' })
  async session(): Promise<ResponseEntity<Session>> {
    return ResponseEntity.ok().body(ContextProvider.getSession());
  }
}

API Key 인증 시스템

구현 내용

API Key 발급

  • • 1분간 유효한 임시 API 키 생성
  • • 보안을 위한 짧은 만료 시간
  • • 서버 측에서 안전한 키 생성
  • • 클라이언트에 즉시 전달

보안 특징

  • • 짧은 만료 시간으로 보안 강화
  • • 서버 측 키 생성으로 안전성 확보
  • • HTTPS를 통한 안전한 전송
  • • 키 재사용 방지

사용 사례

  • • 임시 API 접근 권한 부여
  • • 테스트 환경에서의 API 키 발급
  • • 개발자 도구에서의 임시 인증
  • • 데모 목적의 API 키 생성

API 테스트

테스트 방법

  • • “POST auth/api-key 호출” 버튼을 클릭
  • • 1분간 유효한 API 키를 발급받습니다
  • • 발급받은 키는 자동으로 검증 탭에 설정됩니다

테스트

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

  /**
   * API 인증
   * */
  @Post('api-key')
  @ApiResponseEntity({ type: GetApiKeyOutDto, summary: '1분 api key 발급' })
  async getApiKey(): Promise<ResponseEntity<GetApiKeyOutDto>> {
    const getApiKeyOutDto = this.authService.getApiKey();

    return ResponseEntity.ok().body(getApiKeyOutDto);
  }

  @Post('api-key-validator')
  @ApiResponseEntity({ type: GetApiKeyOutDto, summary: '1분 api key 발급' })
  @ApiSecurity('apiKey')
  @UseGuards(ApiKeyAuthGuard)
  async apiKeyValidator(): Promise<ResponseEntity<string>> {
    return ResponseEntity.ok().body('유효한 api key 확인 완료');
  }
}

소셜 로그인 시스템

구현 내용

OAuth 2.0 프로세스

  • • Authorization URL 생성
  • • 팝업 창으로 인증 요청
  • • Authorization Code 수신
  • • 백엔드로 Code 전송

보안 기능

  • • CSRF 방지를 위한 State 파라미터
  • • Origin 검증
  • • 팝업 차단 감지
  • • 에러 처리 및 재시도

사용자 정보

  • • 이메일 주소
  • • 프로필 이름
  • • 프로필 이미지
  • • Google ID

API 테스트

테스트 방법

  • 팝업 차단 해제: 브라우저에서 팝업을 허용해주세요
  • Google 계정: 로그인할 Google 계정을 준비해주세요
  • 권한 승인: Google에서 요청하는 권한을 승인해주세요

테스트


@ApiTags('auth')
@Controller('auth')
export class AuthController {
  /**
   * OAUTH 인증 - Firebase 방식
   * */
  @Post('google/firebase/login')
  @ApiResponseEntity({
    type: AuthSignUserOutDto,
    summary: 'Firebase Google 로그인',
  })
  async googleFirebaseLogin(
    @Body() googleFirebaseLoginInDto: GoogleFirebaseLoginInDto,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const authSignUserOutDto = await this.authService.googleLoginWithIdToken(
      googleFirebaseLoginInDto.idToken,
    );

    return ResponseEntity.ok().body(authSignUserOutDto);
  }

  /**
   * OAUTH 인증 - Google OAuth 2.0 직접 구현
   * */
  @Post('google/oauth/login')
  @ApiResponseEntity({
    type: AuthSignUserOutDto,
    summary: 'Google OAuth 2.0 직접 로그인',
  })
  async googleOAuthLogin(
    @Body() googleOauthLoginInDto: GoogleOauthLoginInDto,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const authSignUserOutDto = await this.authService.googleLoginWithCode(
      googleOauthLoginInDto.code,
    );

    return ResponseEntity.ok().body(authSignUserOutDto);
  }
}

RBAC (Role-Based Access Control) 시스템

구현 내용

RBAC 시스템

  • • JWT 토큰 기반 인증
  • • 역할 기반 접근 제어
  • • 동적 권한 변경
  • • 권한 변경 시 토큰 자동 갱신

사용자 역할

  • ADMIN: 모든 권한
  • MANAGER: 관리 권한
  • DEVELOPER: 개발 권한
  • GUEST: 읽기 권한

보안 기능

  • • JWT 서명 검증
  • • 토큰 만료 시간 관리
  • • 권한 정보 토큰에 포함
  • • 실시간 권한 검증

API 테스트

테스트 방법

  • 회원가입/로그인: 이메일과 비밀번호로 계정 생성
  • 권한 변경: 원하는 역할을 선택하여 변경
  • 토큰 갱신: 권한 변경 시 새로운 토큰이 자동 발급

회원가입/로그인

권한 변경

@ApiTags('auth')
@Controller('auth')
export class AuthController {
  /**
   * RBAC 권한 관리
   */
  @Put('role')
  @ApiResponseEntity({ type: UserDto, summary: '관리자 권한 변경' })
  @ApiBearerAuth('jwt')
  @UseGuards(JwtAuthGuard)
  async updateRole(
    @Body() updateRoleUserDto: UpdateRoleUserDto,
  ): Promise<ResponseEntity<AuthSignUserOutDto>> {
    const authSignUserOutDto = await this.authService.updateRole(updateRoleUserDto);
    return ResponseEntity.ok().body(authSignUserOutDto);
  }
}

Throttler (Rate Limiting) 시스템

구현 내용

Rate Limiting

  • • IP 기반 요청 제한
  • • Redis 기반 분산 저장소
  • • 다중 Pod 환경 지원
  • • 동적 제한 정책

Throttler 설정

  • 기본 제한: 1분에 10번 요청
  • 엄격 제한: 1분에 3번 요청
  • 짧은 간격: 10초에 5번 요청
  • 제한 없음: @SkipThrottle()

Redis Storage

  • • 분산 환경 지원
  • • 실시간 제한 추적
  • • 자동 만료 처리
  • • 다중 서버 동기화

API 테스트

테스트 방법

  • 기본 제한: 15번 요청 (1분에 10번 제한)
  • 엄격 제한: 10번 요청 (1분에 3번 제한)
  • 제한 없음: 20번 요청 (@SkipThrottle)
  • 짧은 간격: 10번 요청 (10초에 5번 제한)

Rate Limiting 테스트

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [
        {
          ttl: 60000,        // 1분
          limit: 10,         // 10번 요청
        },
      ],
      storage: new RedisThrottlerStorage(),
    }),
    providers: [
      // { provide: APP_GUARD, useClass: ThrottlerGuard }, // 개별 적용으로 변경
    ],
  ],
})
export class ApiModule {}

요청 검증 시스템

구현 내용

Validation (유효성 검사)

  • • 데이터 타입 검증 (@IsString, @IsNumber, @IsBoolean)
  • • 선택적 필드 처리 (@IsOptional)
  • • 형식 검증 (@IsEmail, @IsUrl, @IsDateString)
  • • 범위 검증 (@Length, @Min, @Max, @IsPositive)
  • • 열거형 검증 (@IsIn)
  • • 상수 열거형 검증 (@Validate, ConstantsValidator)

복합 데이터 검증

  • • 배열 검증 (@IsArray, @IsString)
  • • 중첩 객체 검증 (@ValidateNested, @Type)
  • • 객체 배열 검증 (@ValidateNested)
  • • 재귀적 검증 구조

XSS 방지 효과

  • • 입력 길이 제한으로 악성 스크립트 차단
  • • 타입 검증으로 HTML 태그 필터링
  • • 형식 검증으로 잘못된 URL/이메일 차단
  • • 열거형 검증으로 허용되지 않은 값 차단

API 테스트

테스트 방법

  • 직접 입력: 각 필드에 원하는 값을 직접 입력
  • 유효한 데이터: 모든 검증 규칙을 만족하는 값 입력
  • 유효하지 않은 데이터: 검증 규칙을 위반하는 값 입력
  • XSS 공격 시도: 악성 스크립트나 HTML 태그 입력
  • 복합 데이터: 배열, 객체, 객체 배열 형태로 입력

사용자 직접 입력

범위: 1 ~ 100
허용값: true, false
허용값: USER, ADMIN, GUEST
허용값: 0 (None), 1 (Test), 2 (Live)
형식: YYYY-MM-DD (예: 2024-01-01)
export class NestedObjectDto {
  @ApiProperty()
  @IsString()
  @Length(1, 50)
  string: string;

  @ApiProperty()
  @IsNumber()
  @Min(0)
  @Max(100)
  number: number;
}

export const VALIDATION_CONST_ENUM = {
  None: 0,
  Test: 1,
  Live: 2,
};
export type ValidationConstEnum =
  (typeof VALIDATION_CONST_ENUM)[keyof typeof VALIDATION_CONST_ENUM];

export class ValidationInDto {
  // 기본 문자열 검증
  @ApiProperty({ description: '기본 문자열 (1-50자)' })
  @IsString()
  @IsOptional()
  @Length(1, 50)
  string?: string;

  // 이메일 검증
  @ApiProperty({ description: '이메일 형식' })
  @IsEmail()
  @IsOptional()
  email?: string;

  // 숫자 검증
  @ApiProperty({ description: '양수 (1-100)' })
  @IsNumber()
  @IsOptional()
  @Min(1)
  @Max(100)
  number?: number;

  // 불린 검증
  @ApiProperty({ description: '불린 값' })
  @IsBoolean()
  @IsOptional()
  boolean?: boolean;

  // URL 검증
  @ApiProperty({ description: 'URL 형식', required: false })
  @IsOptional()
  @IsUrl()
  url?: string | null;

  // 날짜 문자열 검증
  @ApiProperty({ description: '날짜 문자열 (ISO)', required: false })
  @IsOptional()
  @IsDateString()
  date?: string | null;

  // 열거형 검증
  @ApiProperty({ description: '역할 선택', enum: ['ADMIN', 'USER', 'GUEST'] })
  @IsIn(['ADMIN', 'USER', 'GUEST'])
  @IsOptional()
  in?: string;

  // 상수 열거형 검증
  @ApiProperty({
    description: constToString(VALIDATION_CONST_ENUM, 'VALIDATION_CONST_ENUM'),
  })
  @IsNumber()
  @IsOptional()
  @Validate(ConstantsValidator, [VALIDATION_CONST_ENUM])
  constEnum?: ValidationConstEnum;

  // 배열 검증
  @ApiProperty({ description: '문자열 배열', type: [String] })
  @IsArray()
  @IsOptional()
  @IsString({ each: true })
  @Length(1, 20, { each: true })
  array?: string[];

  // 중첩 객체 검증
  @ApiProperty({ description: '중첩 객체', type: NestedObjectDto })
  @IsObject()
  @IsOptional()
  @ValidateNested()
  @Type(() => NestedObjectDto)
  object?: NestedObjectDto;

  // 중첩 객체 배열 검증
  @ApiProperty({ description: '중첩 객체 배열', type: [NestedObjectDto] })
  @IsArray()
  @IsOptional()
  @ValidateNested({ each: true })
  @Type(() => NestedObjectDto)
  objectArray?: NestedObjectDto[];
}

SQL 인젝션 방지 시스템

구현 내용

TypeORM 보안 기능

  • • Query Builder 사용으로 자동 이스케이프
  • • 파라미터화된 쿼리 (Prepared Statements)
  • • Repository 패턴으로 안전한 DB 접근
  • • 악성 SQL 코드를 단순한 문자열 데이터로 처리

방어 원리

  • • SQL과 데이터가 완전히 분리됨
  • • ? 플레이스홀더로 파라미터 전달
  • • DROP TABLE 등 악성 명령어가 검색어로 처리
  • • 데이터베이스 레벨에서 안전하게 보호

SQL 인젝션 테스트

테스트 방법

  • • 악성 SQL 코드를 입력하여 공격 시도
  • • TypeORM이 어떻게 방어하는지 확인
  • • 실제 실행되는 안전한 쿼리 구조 확인

악성 입력 테스트

예시: '; DROP TABLE users; --, admin' OR '1'='1, UNION SELECT * FROM users
@Controller('auth')
export class AuthController {
  /**
   * sql injection
   */
  @Post('sql-injection')
  @ApiResponseEntity({ summary: 'sql-injection' })
  async sqlInjection(
    @Body() sqlInjectionInDto: SqlInjectionInDto,
  ): Promise<ResponseEntity<unknown>> {
    await this.authService.sqlInjection(sqlInjectionInDto.query);
    return ResponseEntity.ok().build();
  }
}