JWT 토큰만 이용해보려 한 로그인의 고찰?

sally·2023년 1월 12일
1

프로젝트

목록 보기
4/5

JWT 와 로그인 ?

  • 지난 프로젝트에서 로그인을 미루었습니다.
    • ArgumentResolver로 대체하려니 파라미터라도 넘겨주는게 좋은데, 클라이언트에겐 불편해서 FakeAuthUser로 컨트롤러 마다 DI 해서 서비스로 인터페이스 통해 넘겨줬는데요. 컨트롤러는 많아지고 .. 미루었습니다.
  • 세션 로그인은 stateful 해서 제가 생각한 서버 환경과는 맞지 않았어요.
  • 임시 프로젝트로 이번에는 시큐리티를 최소화로 사용하면서 구현해보고자 생각 이었습니다.
    • 필터 통해 컨트롤러의 파라미터로 전달해주는 Authentication 딱 1개.
      • 😮‍💨 Authentication이 시큐리티 인 걸.. (잘못된 시작은...로드맵이 펼쳐지더라구요.)

에러 시작

이전의 임시 대처 흔적들

  • 가입, 로그인 하면서 토큰 전달까지 확인 했습니다.
  • 뒤에 나오지만, 이 프로젝트는 임시였거든요. ddl-auto 를 재미나게🎵 사용 했습니다.
    • UserRole 로 변경 하면서 @Enumerated(EnumType.STRING)를 빠뜨렸고, DB는 int 컬럼으로 생성 되면서
    • 시큐리티 사용하면서 JwtTokenFilter 에서 role 전달에 문제가 생겼습니다.
    • 보면 알 수 있지만, 구체적으로 생각해본 적 없는 연결이었습니다. (그렇게 시작 되더군요.)

로그인한 사용자 Authentication

Post 작성 구현하면서 끝난 거 같다 생각하던 때

로그인한 사용자만 포스트 작성한다.

위와 같은 요구사항을 위해서,
제가 처음 목표한 Authentication 을 컨트롤러 파라미터로 전달 하고자 필터 하나 구현 했습니다.

JwtTokenFilter

JwtTokenFilter 는 위와 같이 HttpServletRequest 의 헤더값을 이용해서 토큰을 가져옵니다.
그 다음은 토큰을 통해 토큰에 담긴 내용으로부터 검증 로직을 거쳐 SecurityContext에 Authentication 객체로 보관합니다.

간략히 그린 시퀀스 다이어그램 보이는 그대로(이길 바라며...)
JwtTokenFilter 에서

  • JwtTokenUtile 통해 토큰에 보관한 nickName을 반환해주고
  • UserService 통해서 nickName 통한 서버 쪽 DB 조회로 사용자 정보를 가져옵니다.
  • SecurityContext 에 보관합니다.

SecurityContextHolder 관련 외에는 제가 임의로 구현했습니다.

  • loadUserByUserName 은 UserDetailsService 구현 방법이 있는데, 그냥 메서드명만 동일합니다. (그렇게 문제로 연결이 되었죠... )

Authentication

PostController 에서 authentication.getName() 으로 전달

  @PostMapping
  public Response<Post> register(PostCreationRequest postCreationRequest, 
  								Authentication authentication) {
     Post post = postService.create(postCreationRequest, authentication.getName());
     return Response.success(post);
  }

에러 발생합니다.
에러는 PostService 에서 전달받은 nickName 통한 UserEntity 조회 중 발생합니다.

디버깅 해보니
먼저 PostController 에서 Authentication 의 principal 안 nickName 이 잘 보입니다.

문제는 PostService 로 전달 되는 객체값이 참조값으로 String 문자열로 반환되지 않았습니다.

디버깅을 다시 돌리며 앞에서의 로직을 확인하는데

PostController 에서의 authentication.getName() 순간 위와 같은 조건문에서 타입 체크를 다 넘어가고 null 은 아닌데, toString()이 참조값으로 넘어갔습니다.

  • 없는 값으로 조회요청까지 간 거죠.

즉 앞서 말했던, loadUserByUserName 을 제가 구현하면서 제가 만든 객체 타입으로 SecurityContext에서 Authentication 통해 이용할 시그니처만 허용되는 상황 인거 같았어요.

  • getName()이 시큐리티에서 보통 username 으로 쓰는 키값도 저는 nickNme로 커스텀하게 썼으면서 어떻게 가져올지 생각하면 당연한 결과 였어요.
  • 제가 구현한 User 객체에 Authentication에서 이용할 타입과 오퍼레이션을 갖게 했습니다.
  • UserDetails 는 메서드가 많아서 AuthenticationPrincipal 을 구현했고, 문제 해결 됐습니다. 추상화를 제대로 쓸 때의 장점
    • JwtTokenFilter 코드 변경 없다.
    • DTO 지만 익명 클래스 등 쓰면서 불변 객체로 구현

시큐리티 알고 가야 하나...

Authentication 을 어떻게 이용할 수 있을까?

UsernamePasswordAuthenticationToken 통해서 연결되었더라구요.
위의 코드에 나온 authentication.getName() 은 AbstractAuthencationToken 에서 오버라이딩해서 타입체크로 이용하고 있었습니다.

  • 제가 구현한 코드나 방식과 무관하고, 제가 오류 수정하면서 검색하면서 부분만 봤지만, 나중에 다시 봐볼 겸 자세한 거 같아서 참조해 봅니다.

이분 블로그 보니 포스트맨 Authorizaion 을 직접 등록하신 것 같은데,..
겸사겸사 첨부하면, 포스트맨에 타입 별 토큰 전달 방식이 있습니다.

  • Authorization 은 타입과 credentials 을 담습니다.
    • 이는 타입과 토큰으로 타입은 기본적으로 Basic 과 Bearer 이 있는 거 같아요.
    • Bearer 는 OAuth 관련 있는 듯 한데, 목록에는 OAuth 버전별 타입이 별도로 있습니다.
    • 저는 비밀키 등 넣어놔서 일반적으로 많이 사용하는 듯한 Bearer 를 사용해봤습니다.
      - Bearer 가 위에서 헤더값 통해 가져온 토큰 판별에도 사용합니다.
    • HTTP Authorization header에 Bearer와 jwt 중 무엇을 사용할까?

끝인 줄 알았지 ...

에러 메시지가 제가 만든 Response 형식이 아니었습니다. Restful...

  • 처음에는 PostController 까지 넘어간 에러 였지만,
  • 토큰 제외하고 확인하는 과정에서 다른 에러를 보게 됐네요.
    • '에러 발생 맞네' 하고 넘어갈 뻔 했습니다.

무지성이라 시큐리티 필터 부분은 점점 자신이 작아지드라구요.
블로그 참고 하면서 넘어 갈 수 있었습니다.


SecurityContext ?

에러 핸들링까지 별개로 처리하는 걸 보며 왜지.. 란 궁금증이 등 돌리면 떠올라서 정리 해봤습니다.

먼저 시큐리티 필터 관련한 내용을 간략히만 하고 넘어갈께요.
많은 블로그 예제에서의 WebSecurityConfigureAdpter 는 deprecated 됐고 체인방식으로 변경 됐습니다.

@Configuration
@EnableWebSecurity
public class AuthenticationConfiguration {
   @Bean
   public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
      return httpSecurity....
      		.exceptionHandling()
			.authenticationEntryPoint(new MyAuthenticationEntryPoint())
            .build();
	}
}

위와 같이 filterChain 통해서 SecurityFilterChain로 반환되게만 해주면 되고,
체이닝 방식은 거의 유사 합니다.
여기서 위와 같이 제가 JwtTokenFilter 에 대한 에러 핸들링을 추가 했습니다.

security의 filterChain 에서 authenticationEntryPoint() 로 연결 시킵니다.

  • 스프링 시큐리티 컨텍스트 인증과정에서 에러 발생시 AuthenticationEntryPoint 가 예외 핸들링 하는 인터페이스 라고 하네요.
  • 저는 위의 블로그 내용처럼 AuthenticationEntryPoint 구현하면서 응답형태를 반환하게 해요.

제가 좀더 봐보고자 싶어진 부분 !

	public abstract class AuthenticationException extends RuntimeException { 
    }

RuntimeException?
제가 @RestControllerAdvice 통해서 RuntimeException을 추가해놨거든요.
필터니까 당연하다 생각되지만,
시큐리티 컨텍스트가 스프링의 앞단에서 임의 처리 한다면,
그러면 스프링의 필터하고는 어떤 순..서? 어떻게 시큐리티는 라이브러리 추가로 이렇게 작동하게 했지?
(알기 힘들거 같지만 용기를...)

스프링의 순서는 아래와 같습니다.
(MVC 기준입니다. 김영한님의 MVC2 강의 중 일부입니다. ㅎㅎ)

HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러

  • 필터 제한으로 필터에서 적절하지 않은 요청이라고 판단하면 서블릿 호출 안하고 필터에서 끝낼 수 있습니다.
  • 스프링 부트에서는 ExceptionHandlerExceptionResolver 제공으로 @ExceptionHandler 통한 처리를 합니다.

그러면, 스프링 시큐리티와 스프링의 필터 제한 방식과 코드 차이는 ?

  • 제 기억으로는…. MVC 모델에서는 filterChain.doFilter(request, response); 를 던지지 않아요.
  • 저의 JwtTokenFilter 코드는 에러든 성공이든 모든 경우에 filterChain.doFilter(request, response); 계속 던집니다. (내가 잘못 했나?ㅎㅎ)

시큐리티 검색하면 크게 3종류 이미지들이 많이 보이는데 그 중 하나 입니다.

  • 스프링 시큐리티가 제공하는 서블릿 필터로 DelegatingFilterProxy 에 의해 위임되어 동작합니다.
  • DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain순으로 Filter가 진행 되나 봐요.

p.s.
스프링과 달리 스프링 부트는 내장 웹서버 형식으로 필터 빈 등록과정이 다를 수 있습니다.

역시, 공식 문서

ExceptionTranslationFilter는 FilterChainProxy 에 의해 추가된 보안필터로서
AuthenticationException 던져져도 filterChain.doFilter(request, response) 을 계속 호출하며 작업을 이어 처리 합니다.
이후, 인증 처리를 시작해 SecurityContextHolder 에서 AuthenticationEntryPoint 까지 연결 됩니다.

  • 인증 관련 오류까지도 SecurityContextHolder 에서 관리하는 것 같습니다.

이 과정에서 SecurityContextHolder 를 비웁니다.

2번의 SecurityContextHolder.clearContext()가 실행 되더라구요.

SecurityContextPersistenceFilter → ThreadLocalSecurityContextHolderStrategy
FilterChainProxy → ThreadLocalSecurityContextHolderStrategy

JwtTokenFilter 에서 빈 토큰으로 인해 if문 블럭으로 넘어가서, 에러 발생으로 filterChain.doFilter(request, response) 넘겨지고, ExceptionTranslationFilter 에서 SecurityContext 를 새로 생성합니다.

이후 다시 JwtTokenFilter의 catch 블락에서 filterChain.doFilter(request, response); 호출 합니다.

SecurityContextPersistenceFilter 에서 지웁니다.

  • 이는 ThreadLocalSecurityContextHolderStrategy 까지 가서 ThreadLocal 에서 remove() 합니다.

FilterChainProxy 에서의 clearContext() 로 ThreadLocalSecurityContextHolderStrategy 까지 갑니다.

ThreadLocalSecurityContextHolderStrategy?

공식문서에서 스레드 경합을 피하기 위해서는 SecurityContextHolder.createEmptyContext() 형태로 생성하라고 합니다.

  • 삭제 과정으로 예상하면, SecurityContextHolderStrategy 인터페이스 통해서 ThreadLocalSecurityContextHolderStrategyThreadLocal 로 생성하는 거 같아요.

    • SecurityContextHolder.getContext().setAuthentication(authentication) 는 안 좋다네요. (제가 이렇게 구현을 해써요..)
  • 에러 처리에서 SecurityContextHolder.createEmptyContext() 를 호출?

    • SecurityContext 에 Authentication 객체를 담기전에 JwtTokenFilter 에서 에러가 발생 한 상황으로 생성하면서 까지 관리하는 가 싶었는데요.
    • 구현 코드로는 Filter 인터페이스는 스프링인데, 시큐리티로 연결 된 점이 신기 했습니다.
  • 그런데 2번의 삭제?

    • 스프링의 Filter 체인 속에서 DelegatingFilterProxy를 호출 하고, FilterChainProxy 는 빈으로 관리되는 객체로서 SecurityFilterChain 으로 연결됩니다. 스프링 필터체인을 빠져나와 시큐리티 필터체인들 중 SecurityContextPersistenceFilter 는 스프링 세션에 있던 SecurityContext 를 SecurityContextHolder로 넘기면서 영속화 하는 거 같아요.
    • 그리고 시큐리티 필터 체인 중에서 에러 발생하거나 인증성공시 각각 SecurityContextHolder로 넘어갑니다.
    • 그렇게 2 영역에서 clear 가 일어나는 걸까 .. 생각해 봅니다. (오늘도... 저는 모르겠네요 ㅎㅎㅎㅎ)
    • SecurityContextPersistenceFilter 알아보기

  • 스레드 경합을 피하기 위해 SecurityContextHolder.getContext().setAuthentication(authentication) 는 안 좋다 ?
    • 사용자 마다 Authenticaiton 객체를 사용하고자 한다면 SecurityContextHolder.createEmptyContext() 로서 스레드 별 Authentication 을 관리해야 하는지에 대해서는 로그인 후면 시스템에서의 인증 절차에 따라 다를거 같지만,
      보통 사용자 정보 변경 보다는 조회가 많을 거 같고, 그러면 스레드 병합을 고려해야 할까 싶은데,...
    • ThreadLocal 해제는 중요하고, SecurityContext의 clearContext() 가 통째로 일어나는 거 보면, 로직상 SecurityContextHolder.createEmptyContext() 로 해줘야 하는 건가 싶기도 합니다.
      • SecurityContextHolderStrategy 구현체마다 다르게 진행되겠죠? (모르겠어요 😢)
  • SecurityContextHolder 3가지 전략
    • SecurityContextHolder.MODE_GLOBAL
      • JVM 내 모든 스레드들이 같은 Security Context를 이용
    • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
      • 스레드 전파 ? OS 에서 권한이 자식스레드 한테도 던달 되는 것과 유사한 거 같아요.
    • (default) SecurityContextHolder.MODE_THREADLOCAL

..... 긴 글 읽어주신 분들은 감사합니다.



Spring Security

profile
sally의 법칙을 따르는 bug Duck

0개의 댓글