ddl-auto
를 재미나게🎵 사용 했습니다.@Enumerated(EnumType.STRING)
를 빠뜨렸고, DB는 int 컬럼으로 생성 되면서 Post 작성 구현하면서 끝난 거 같다 생각하던 때
로그인한 사용자만 포스트 작성한다.
위와 같은 요구사항을 위해서,
제가 처음 목표한 Authentication 을 컨트롤러 파라미터로 전달 하고자 필터 하나 구현 했습니다.
JwtTokenFilter 는 위와 같이 HttpServletRequest 의 헤더값을 이용해서 토큰을 가져옵니다.
그 다음은 토큰을 통해 토큰에 담긴 내용으로부터 검증 로직을 거쳐 SecurityContext에 Authentication 객체로 보관합니다.
간략히 그린 시퀀스 다이어그램 보이는 그대로(이길 바라며...)
JwtTokenFilter 에서
SecurityContextHolder 관련 외에는 제가 임의로 구현했습니다.
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 통해 이용할 시그니처만 허용되는 상황 인거 같았어요.
nickNme
로 커스텀하게 썼으면서 어떻게 가져올지 생각하면 당연한 결과 였어요.UsernamePasswordAuthenticationToken
통해서 연결되었더라구요.
위의 코드에 나온 authentication.getName() 은 AbstractAuthencationToken 에서 오버라이딩해서 타입체크로 이용하고 있었습니다.
이분 블로그 보니 포스트맨 Authorizaion 을 직접 등록하신 것 같은데,..
겸사겸사 첨부하면, 포스트맨에 타입 별 토큰 전달 방식이 있습니다.
Bearer
가 위에서 헤더값 통해 가져온 토큰 판별에도 사용합니다.에러 메시지가 제가 만든 Response 형식이 아니었습니다. Restful...
무지성이라 시큐리티 필터 부분은 점점 자신이 작아지드라구요.
블로그 참고 하면서 넘어 갈 수 있었습니다.
에러 핸들링까지 별개로 처리하는 걸 보며 왜지.. 란 궁금증이 등 돌리면 떠올라서 정리 해봤습니다.
먼저 시큐리티 필터 관련한 내용을 간략히만 하고 넘어갈께요.
많은 블로그 예제에서의 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() 로 연결 시킵니다.
제가 좀더 봐보고자 싶어진 부분 !
public abstract class AuthenticationException extends RuntimeException {
}
RuntimeException?
제가 @RestControllerAdvice
통해서 RuntimeException을 추가해놨거든요.
필터니까 당연하다 생각되지만,
시큐리티 컨텍스트가 스프링의 앞단에서 임의 처리 한다면,
그러면 스프링의 필터하고는 어떤 순..서? 어떻게 시큐리티는 라이브러리 추가로 이렇게 작동하게 했지?
(알기 힘들거 같지만 용기를...)
스프링의 순서는 아래와 같습니다.
(MVC 기준입니다. 김영한님의 MVC2 강의 중 일부입니다. ㅎㅎ)
HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
- 필터 제한으로 필터에서 적절하지 않은 요청이라고 판단하면 서블릿 호출 안하고 필터에서 끝낼 수 있습니다.
- 스프링 부트에서는 ExceptionHandlerExceptionResolver 제공으로 @ExceptionHandler 통한 처리를 합니다.
그러면, 스프링 시큐리티와 스프링의 필터 제한 방식과 코드 차이는 ?
filterChain.doFilter(request, response);
를 던지지 않아요.filterChain.doFilter(request, response);
계속 던집니다. (내가 잘못 했나?ㅎㅎ)시큐리티 검색하면 크게 3종류 이미지들이 많이 보이는데 그 중 하나 입니다.
p.s.
스프링과 달리 스프링 부트는 내장 웹서버 형식으로 필터 빈 등록과정이 다를 수 있습니다.
ExceptionTranslationFilter는 FilterChainProxy 에 의해 추가된 보안필터로서
AuthenticationException 던져져도 filterChain.doFilter(request, response) 을 계속 호출하며 작업을 이어 처리 합니다.
이후, 인증 처리를 시작해 SecurityContextHolder 에서 AuthenticationEntryPoint 까지 연결 됩니다.
2번의 SecurityContextHolder.clearContext()가 실행 되더라구요.
SecurityContextPersistenceFilter → ThreadLocalSecurityContextHolderStrategy
FilterChainProxy → ThreadLocalSecurityContextHolderStrategy
JwtTokenFilter 에서 빈 토큰으로 인해 if문 블럭으로 넘어가서, 에러 발생으로 filterChain.doFilter(request, response)
넘겨지고, ExceptionTranslationFilter 에서 SecurityContext 를 새로 생성합니다.
이후 다시 JwtTokenFilter의 catch 블락에서 filterChain.doFilter(request, response);
호출 합니다.
SecurityContextPersistenceFilter 에서 지웁니다.
FilterChainProxy 에서의 clearContext() 로 ThreadLocalSecurityContextHolderStrategy 까지 갑니다.
공식문서에서 스레드 경합을 피하기 위해서는 SecurityContextHolder.createEmptyContext() 형태로 생성하라고 합니다.
삭제 과정으로 예상하면, SecurityContextHolderStrategy
인터페이스 통해서 ThreadLocalSecurityContextHolderStrategy
의 ThreadLocal
로 생성하는 거 같아요.
에러 처리에서 SecurityContextHolder.createEmptyContext() 를 호출?
그런데 2번의 삭제?
..... 긴 글 읽어주신 분들은 감사합니다.