
지난 글에 이어 JWT를 설정할텐데요, 실제 작동할 필터를 만들고 이 필터를 시큐리티에 등록해보겠습니다.
먼저 필터는 UsernamePasswordAuthenticationFilter를 상속해 커스텀하여 사용합니다.

먼저 AuthenticationManager를 파라미터로 받는 생성자를 만듭니다. 나중에 필터를 시큐리티에 등록할 때 이 AuthenticationManager 객체가 꼭 필요하기 때문입니다. 그리고 setFilterProcessesUrl을 통해 시큐리티가 낚아채는 URL 경로를 재설정해주는데요, 그 이유는 기본적으로 시큐리티가 낚아채는 URL이 "/login"으로 되어 있기 때문입니다. 저희는 기본적으로 경로의 앞에 "/api"를 붙이기 때문에 재설정해주었습니다.
그리고 우리가 하려는 방식은 세션을 저장하는 방식의 로그인이 아니기 때문에, 사용자를 강제로 로그인 상태로 만들어야 하는데요, 그러기 위해 AbstractAuthenticationProcessingFilter의 attemptAuthentication() 매서드를 오버라이딩했습니다. 해당 매서드의 내용은 아래와 같습니다.
- 요청으로 온 데이터를 LoginRequestDto로 변환한다.
- 해당 데이터로 UsernamePasswordAuthenticationToken을 반환한다.
- AuthenticationManager의 authenticate() 매서드를 이용해 loadUserByUsername()을 호출하고, 최종적으로 Authentication 객체를 반환한다.
그리고 이렇게 해서 반환된 Authentication 객체의 정보를 가지는 JWT 토큰을 만들어야 합니다.

successfulAuthentication() 매서드도 위와 마찬가지로 AbstractAuthenticationProcessingFilter의 매서드를 오버라이딩한 것으로, attemptAuthentication()가 성공적으로 실행되면 이어 호출되는 매서드입니다. 매서드의 내용은 아래와 같습니다.
- attemptAuthentication()에서 반환된 Authentication 객체에서 Principal을 가져와 CustomUserDetails 객체를 반환합니다.
- CustomUserDetails의 정보를 가진 JWT를 생성합니다.
- 응답 헤더에 "Authorization": "토큰"을 추가합니다.
- CustomUserDetails 객체를 LoginResponseDto으로 변환해 응답합니다.
위에서 사용된 CustomResponseUtil.success(response, loginResponseDto);의 내용은 아래와 같습니다.

이제 JWT를 생성하는 로직과 시큐리티 필터가 구성되었으니 시큐리티에 이 필터를 설정해보겠습니다. 먼저 SecuritConfig 내에 AbstractHttpConfigurer를 상속하는 CustomSecurityFilterManager 클래스를 만들어야 하는데요, 그 이유는 뒤에 가서 말씀드리겠습니다.

- CustomSecurityFilterManager 클래스를 만들고 자기 자신과 HttpSecurity를 제네릭으로 가진 AbstractHttpConfigurer를 상속합니다.
- configure() 매서드를 오버라이딩하는데, HttpSecurity 객체를 구성하기 위함입니다. 이 안에서 필터를 1차적으로 등록해야 합니다.
- builder 객체에서 AuthenticationManager 객체를 가져옵니다. 오직 필터의 생성자에 넣기 위함입니다. HttpSecurity는 시큐리티에서 공유되는 객체입니다.
- builder 객체에 JWT 필터를 추가합니다.
그리고 위에서 만든 CustomSecurityFilterManager 객체를 SecurityFilterChain 내에 등록해주어야 합니다.

이렇게 되면 시큐리티와 JWT를 발급하는 필터가 등록된 것입니다.
위에서 말했듯 이제 CustomSecurityFilterManager를 별도로 만드는 이유에 설명할텐데요, 이 작업이 필수는 아니었습니다. 다만 코드의 가독성을 위한 것이었는데요, AbstractHttpConfigurer를 상속한 CustomSecurityFilterManager를 별도로 만들지 않았다면 http.apply() 내부에서 AbstractHttpConfigurer를 구현해야 하기 때문입니다. 그러니까 아래와 같은 모습으로 말이죠.

이제 필터 등록까지 마쳤으니 포스트맨으로 로그인 요청을 해보겠습니다.

정상적인 요청은 200 성공 응답이 온 것을 확인할 수 있습니다. 하지만 패스워드를 빼먹은 요청은 401에러가 난 것을 볼 수 있는데요, 이 실패에 대한 응답도 성공에 대한 응답과 같은 형식을 응답할 수 있게 설정해보겠습니다. 이것도 JwtAuthenticationFilter 필터에 등록합니다.

로그인에 실패하면 InternalAuthenticationServiceException 예외가 던져지는데요, 이 예외는 AuthenticationException 예외를 상속합니다. 그래서 unsuccessfulAuthentication()의 3번째 파라미터에 담기게 되고 이 매서드가 호출되는 겁니다. 이전에 만들었던 CumtomResponseUtil의 noAuthentication() 매서드를 사용해 반환합니다. 그리고 포스트맨으로 요청해보면 정돈된 모습으로 실패 응답이 온 것을 볼 수 있습니다.

다음 글에서는 JWT의 생성과 검증 코드를 테스트하고, 위에 나온 successfulAuthentication()와 unsuccessfulAuthentication()를 테스트해보겠습니다.