트러블 슈팅_미니 프로젝트(뉴스 피드) (1)

star_pooh·2024년 12월 26일
1

에러 대응기

목록 보기
6/9

미니 프로젝트를 진행하는 과정에서 로그인 기능을 JWT를 이용하여 구현하게 되었는데, 그 때 만났던 에러에 대해서 정리를 하려고 한다.

JWT

왜 Spring Security를 선택했나?

인증/인가에 대해서 튜터님들의 세션을 들을 때마다 Spring Security는 투자하는 노력에 비해 가성비가 좋지 않으니 지금은 신경 쓰지 않아도 된다라는 것이었다. 하지만 Spring Security를 사용하지 않고 구현하는 방법이 머릿 속에 그려지지 않아서 Spring Security를 사용하는 쪽으로 결정했다. 3~4년 전에 복붙 수준으로 한 번 만들어봤던 것이 전부였기 때문에 참고할 자료가 많았으면 좋겠다라고 생각을 했는데 대부분 Spring Security를 사용했기 때문에 정한 것도 이유 중에 하나다.

어떻게 만들 것인가?

JWT의 특징들에 대해서만 알고 있었기 때문에 예제 코드나 구현 내용을 설명해주는 강의 같은게 필요했는데, 인프런에서 튜토리얼을 다루는 강의를 찾게 되었고 하나씩 차근차근 따라해 나갔다. 그런데...

생각치 못한 버전 업그레이드

강의를 따라서 튜토리얼 코드를 구현 중에 하위 메소드가 나오지 않는 것이었다. 검색해보니 버전이 변경 되면서 Deprecated 된 메소드들이었던 것이다. Spring Securityjjwt(JWT를 구현하기 위해 사용한 라이브러리)에서 버전을 확인하고 버전에 따른 구현 방법을 찾았는데, 잘 찾아지지 않아서 JWT를 사용하지 않아도 되는지 상담까지 생각했었다. 마지막이라는 생각으로 강의에 최신 코드가 반영된 부분이 있을까 하고 찾아봤는데 강사님께서 버전에 맞춰서 작성해주신 예제 코드가 깃허브에 있었다!

잘 진행된 코드 작성과 그렇지 못한 실행 결과

어떻게 동작하는지에 대한 이해는 잠시 제쳐두고 깃허브에 있는 내용을 기반으로 코드 작성을 했다. 예제 코드가 꼼꼼하게 작성되어 있었기 때문에 코드를 작성하는데는 문제가 없었다. 다만, Spring Security가 들어가다보니 작성해야 할 양이 이전에 구현했던 Session과 비교해서 몇 배는 많았다. 코드 작성을 다 끝낸후 떨리는 마음으로 실행을 했는데 문제 없이 동작했다. 남은 일은 로그인이 성공적으로 이루어지고 생성된 토큰 값이 확인되는 것이었는데 다음과 같은 에러가 발생하였다. 참고로 진행 중인 프로젝트에서는 이메일과 비밀번호를 이용해서 로그인을 한다.

[newsfeed] [nio-8080-exec-1] o.t.n.exception.GlobalExceptionHandler   : [BadCredentialsException] 500 INTERNAL_SERVER_ERROR : 자격 증명에 실패하였습니다.

// 상세 에러 내용
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:141)
org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
org.team14.newsfeed.controller.AuthController.login(AuthController.java:38)

위의 에러 내용을 기반으로 검색해보니 토큰을 만들때 사용자가 요청으로 전달한 비밀번호가 아닌 암호화된 비밀번호를 전달할 경우 발생하기 때문에 암호화하기 이전의 비밀번호를 사용하면 해결된다고 했는데, 나는 이미 암호화하기 이전의 비밀번호를 사용하기 있었기 때문에 해당하는 내용이 아니었다.

그렇게 원인을 찾아 헤매고 있었는데 같은 에러를 겪으신 분해결 방법을 찾으신 분이 있어서 참고하며 해결했다.

BadCredentialsException의 원인

토큰이 제대로 생성되지 않았기 때문에 상세 에러 내용에 적혀있는대로 토큰을 생성해주는 메소드부터 하나씩 따라가보려고 한다.

토큰을 생성해주는 메소드로 들어가보면 인증을 관리하는 AuthenticationManager 인터페이스를 만나게 된다.

ProviderManager의 authenticate의 구현체로 이동해서 다시 한번 authenticate 메소드로 이동하면

AuthenticationProvider 인터페이스를 만나게 된다.

💡 ProviderManager는 AuthenticationManager, MessageSourceAware, InitializingBean 구현체이다.

AbstractUserDetailsAuthenticationProvider의 구현체로 이동하게 되면 에러 내용에서 봤던 BadCredentialsException을 만나게 된다! 하지만 아직 끝이 아니다. 에러가 발생한 지점을 찾았으니 원인을 찾아야한다. catch문에서 예외가 처리되고 있기 때문에 try문의 retrieveUser 메소드로 이동하면

이렇게 추상 클래스를 만나게 되고

구현체로 이동하게 되면 사용자 이름으로 사용자 정보를 조회하는 loadByUsername이라는 메소드를 만나게 된다.

그런데 위에서 지나치듯 언급했지만, 우리는 이메일로 로그인을 하고 싶은데 username으로 사용자 정보를 조회하기 때문에 에러가 발생한 것 같았다. 직접 코드를 실행시켜서 확인해보니 우려하던 결과가 실제로 눈앞에 보여지게 됐다.

BadCredentialsException의 해결 방법

이메일로 로그인 하고 싶었는데 username으로 조회하는 것이 원인이라는 것까지는 알았는데 어떻게 해결해야하나 고민이 되었다. 그런데 다행인 것은 loadUserByUsername 메소드가 인터페이스라는 것이다.

그렇다는 것은 UserDetailService를 implements하여 loadUserByUsername 메소드를 오버라이딩 할 수 있다는 것이다.

그런데 어떻게 Spring Security랑 연결해주지..?

제목과 같은 고민을 잠깐 했었는데 Spring Security에서는 SecurityAutoConfiguration 클래스에 의해 구성을 설정하는데, 사용자가 정의한 UserDetailsService가 있다면 우선적으로 사용하도록 한다고 한다. 그러니까 CustomUserDetailService를 만들어서 Bean으로 등록한 순간 우리의 역할은 끝난 것이다!

결과는?

원하던 결과를 문제없이 얻을 수 있었고, 이번 트러블 슈팅으로 인해서 왜 Spring Security를 사용하려면 많은 투자가 필요하다고 했는지 느끼게 됐다. 기회가 된다면 Spring Security 없이 JWT를 구현하는 것을 연습해 보면 좋을 것 같다고 생각했다.

그런데 말입니다

자격 증명에 실패했다는 에러를 해결할 수 있는 방법 중에 하나로 암호화하기 이전의 비밀번호(사용자가 입력한 비밀번호)를 사용하는 것이었는데 이것은 왜 그래야하는건지 궁금해졌다.

다시 이동해보자

위의 메소드 파고들기 여정에서 AbstractUserDetailsAuthenticationProvider의 authenticate까지는 동일하다. loadUserByUsername에서 에러가 발생하지 않았다면, 그 이후의 코드가 진행될 것이기 때문에 그 부분을 살펴볼 것이다.

이동하게 되면 추상 클래스인 AbstractUserDetailsAuthenticationProvider를 만나게 된다.

구현체로 이동하게 되면 익숙한 BadCredentialsException을 만나게 된다. BadCredentialsException이 발생하게 되는 조건을 자세히 보면 사용자가 입력한 비밀번호와 조회해온 사용자 정보의 비밀번호가 일치하는지 확인한다. 그렇기 때문에 사용자가 입력한 비밀번호를 그대로 전해주어야 했던 것이었다.



✅ 출처

0개의 댓글

관련 채용 정보