로그인 에러 응답 통일(유저 열거 방지) 및 토큰 단일화 정책 반영

gminnimk·2025년 9월 19일

문제 해결

목록 보기
12/18

문제 상황

기존 로그인 메서드에서

  1. 기존 유저의 아이디 존재 여부 (USER_NOT_FOUND)
  2. 기존 유저의 패스워드 일치 여부 (INVALID_PASSWORD)

를 구분 및 반환하여 사용자 열거 위험을 인지하였습니다.

추가적으로 르그인 시 기존 리프레시 토큰을 삭제하지 않아 재발급 시 정합성/중복 문제가 발생할 수 있음을 발견하였습니다.

이를 해결하고자 검증 로직을 하나로 통합하고 로그인 메서드 호출 시 기존 리프레시 토큰을 삭제 후 재발급 하도록 개선하였습니다.



기존 로그인 메서드

 public JwtResponseDto login(UserLoginRequestDto requestDto) {
        User user = userRepository.findByUsername(requestDto.getUsername())
                .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

        if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
            throw new CustomException(ErrorCode.INVALID_PASSWORD);
        }
 
         UserDetailsImpl userDetails = UserDetailsImpl.build(user);
         String accessToken = jwtUtil.generateAccessToken(userDetails);
         String refreshToken = refreshTokenService.createRefreshToken(user).getToken();
 
         return new JwtResponseDto(accessToken, refreshToken);
     }

개선된 로그인 메서드

 public JwtResponseDto login(UserLoginRequestDto requestDto) {
        User user = userRepository.findByUsername(requestDto.getUsername()).orElse(null);
        if (user == null || !passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
            throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
        }
 
         UserDetailsImpl userDetails = UserDetailsImpl.build(user);
         String accessToken = jwtUtil.generateAccessToken(userDetails);
         // 단일 세션 정책: 기존 리프레시 토큰 제거 후 재발급
         refreshTokenRepository.deleteByUser(user);
         String refreshToken = refreshTokenService.createRefreshToken(user).getToken();
 
         return new JwtResponseDto(accessToken, refreshToken);
     }

  • ErrorCode
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 올바르지 않습니다.")


사용자 열거란?

공격자가 시스템의 응답 차이를 이용해
특정 아이디가 존재하는지 여부를 알아낼 수 있는 보안 취약점.

즉, 공격자가 로그인 요청을 반복하면서 “이 아이디가 존재하는지 / 존재하지 않는지”를 시스템 응답으로 유추할 수 있습니다.



기존 로그인 메서드에서의 문제점

코드 흐름

// 아이디 없음
orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// 비밀번호 불일치
throw new CustomException(ErrorCode.INVALID_PASSWORD);

발생 가능한 사용자 열거 시나리오

  1. 공격자가 username = "testUser"를 입력하고 로그인 시도
    • DB에 testUser가 없으면 → USER_NOT_FOUND 반환
    • 즉, 공격자는 “testUser는 존재하지 않는다”를 알 수 있음.

  2. 공격자가 존재하는 유저의 아이디를 입력하고 잘못된 비밀번호 입력
    • 시스템은 INVALID_PASSWORD 반환
    • 즉, 공격자는 “이 아이디는 존재한다”를 확인할 수 있음.

응답 메시지를 다르게 주었기 때문에 공격자가 사용자 계정 존재 여부를 확인할 수 있는 취약점 발생



개선된 로그인 메서드에서의 대응

if (user == null || !passwordEncoder.matches(...)) {
    throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}

응답 시나리오

  • 아이디가 존재하지 않아도
  • 아이디는 존재하지만 비밀번호가 틀려도

무조건 같은 INVALID_CREDENTIALS 에러만 발생



사용자 열거 공격 예시

공격자 요청 예시

POST /api/users/login
Content-Type: application/json

{ "username": "victimUser", "password": "wrongPass" }

기존 코드일 경우
	•	응답1: {"code": "USER_NOT_FOUND", "message": "존재하지 않는 유저입니다."}
	•	응답2: {"code": "INVALID_PASSWORD", "message": "비밀번호가 올바르지 않습니다."}

공격자는 1번 응답이면 계정 없음, 2번 응답이면 계정 존재라고 확신 가능.


개선된 코드일 경우 - 응답 (공통):
{"code": "INVALID_CREDENTIALS", "message": "아이디 또는 비밀번호가 올바르지 않습니다."}

공격자는 계정 존재 여부를 구분할 수 없음.

0개의 댓글