Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

걸음마부터 달리기

[24/11/14] 시큐리티의 ExceptionTranslationFilter 본문

카테고리 없음

[24/11/14] 시큐리티의 ExceptionTranslationFilter

성추 2024. 11. 14. 19:35

ExceptionTranslationFilter 는 Security에서 발생한 인가와 인증 예외에 대해 예외처리를 수행했다.

AccessDeniedException와 AutheticationException의 예외 타입에 대해서만 처리를 해준다는 것을 앞의 게시물에서 보았다. 

 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
/*        http
                .cors().and()
                .csrf().disable()
                .httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeHttpRequests().requestMatchers("/","/api/v1/auth/**","/api/v1/search/**","/file/**").permitAll()
                .requestMatchers(HttpMethod.GET,"/api/v1/board/**","/api/v1/user/*").permitAll()
                .anyRequest().authenticated().and()
                .exceptionHandling().authenticationEntryPoint(new FailedAuthentication());

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();*/
        http
                .cors(cors -> cors.disable())
                .csrf(csrf -> csrf.disable())
                .httpBasic(httpBasic -> httpBasic.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/api/v1/auth/**", "/api/v1/search/**", "/file/**").permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/*").permitAll()
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exception -> exception
                        .authenticationEntryPoint(new FailedAuthentication())
                );

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}
//spring Security는 인가를 실패하면 기본 오류 페이지를 내보냄.
//근데 API 통신을 하고 있으니 에러 JSON을 내보내야함.
class FailedAuthentication implements AuthenticationEntryPoint { //(1)
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{ \"code:\": \"AF\",\"message\": \"Authorization failed.\"}");
    }
}

문제는 (1)번 부분이다. 시큐리티에서 인증에 실패하면 AutheticationException가 발생하기에 ExceptionTranslationFilter가 이 예외를 잡아서 AuthenticationEntryPoint에서 로직을 수행한다. 

내 상황은 API 서버로 만들고 있기에 Error Json을 보내주기 위해서 AuthenticationEntryPoint의 FailedAuthentication으로 구현했다. 

 

그런데 Postman으로 Validation에 걸리게 보내봤더니 저 FailedAuthentication에서 해당 에러 Json이 출력됐다.

보냈더니
이렇게 나옴

얼탱이가 없었다.

내가 보고있는건 이 구조이기 때문이다. 결국 시큐리티는 필터쪽에서 이루어지니까 사실상 컨테이너와는 연관이 없다. 하지만 DelegatingFilter 덕분에 필터 자체는 스프링 빈으로 등록해버리고 각각에 맞춰 이 빈들에게 위임하는 형식으로 가는데...

 

Validation은 Spring 기술이다. WAS 기술이 아니다. 따라서 Validation에서 MethodArgumentNotValidException 예외를 던지는데 왜 ExceptionTranslationFilter가 AccessDeniedException와 AutheticationException 타입의 예외도 아닌데 잡아서 처리하는거지? ( 왜 잡아서 AuthenticationEntryPoint가 동작하는거지?)

 

1) 현재 컨테이너 쪽에서 / 어플리케이션 쪽에서 내가 예외를 핸들링 하지 않고 있기에 언체크 예외로 우선 시큐리티까지 올라간다. 2) 시큐리티에서도 현재 처리 안하고 있으니까 WAS까지 전파된다.3) WAS까지 왔으니 /error 페이지로 내부적으로 요청을 보낸다.4) 여기서 다시 서블릿 컨테이너 타면서 시큐리티 필터들을 타는데 이때 인증이 안된 상태에서 요청하고 있으니 AutheticationException 예외가 발생한다.5) AutheticationException 가 발생하니 ExceptionTranslationFilter가 동작해서 AuthenticationEntryPoint가 동작한 결과가 해당 JSON 이다.

 

---------------------------------------------------------------------------------------------------------------------------------------------------------

 

대부분 시큐리티에서 커스텀 필터를 사용할때 OncePerRequestFilter를 사용하는걸 볼 수 있다.

왜 굳이 그냥 Filter 인터페이스 받아서 구현하면 되는데 왜 OncePerRequestFilter를 받을까 궁금했다.

 

OncePerRequestFilter는 한번의 Request 요청에 한번만 동작하는 필터이다.

Redirect시 브라우저쪽에서 다시 요청하므로 아예 다른 Request이어서 이때는 OncePerRequestFilter 가 수행되지만 

서버 내부의 Forward시에는 OncePerRequestFilter 가 하나의 Request이기에 수행되지 않는다.

 

따라서 Filter는 중복 수행의 위험이 있어서 굳이 Filter로 구현하지 않고 OncePerRequestFilter 로 구현하는 것이다.

 

 JWT 인증관련 커스텀 필터를 첨부한다.

 

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JWTProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = parseBearerToken(request);
            if (token == null) {
                filterChain.doFilter(request, response);
                return;
            }

            String email = jwtProvider.validate(token);
            if (email == null) {  //검증 안되면
                filterChain.doFilter(request, response);
                return;
            }

            //JWT 인증방식이라 Credentials를 null로...
            AbstractAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, null, AuthorityUtils.NO_AUTHORITIES);
            //WebAuthenticationDetails는 인증과정에서 Authentication 객체에 추가정보 Ip를 추가해줌
            //네트워크로 서블릿 관련 작업이라 try catch
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            securityContext.setAuthentication(authenticationToken);

            SecurityContextHolder.setContext(securityContext);

            filterChain.doFilter(request, response);
        } catch (Exception e){
            e.printStackTrace();
        }

        filterChain.doFilter(request,response);
    }

    //헤더에 오는 Authentication : Bearer 파싱을 여기서 하고 그 파싱된걸 필터에서 jwtProvider가 검증 후 리턴
    private String parseBearerToken(HttpServletRequest request){
        String authorization = request.getHeader("Authorization");
        //null , 공백 , 길이 0 이면 false , 아니면 true
        boolean hasAuthorization = StringUtils.hasText(authorization);
        if(!hasAuthorization) return null;

        boolean isBearer = authorization.startsWith("Bearer ");
        if(!isBearer) return null;
        //여기까지 왔으면 검증은 안됐지만 일단 Bearer 토큰이 있는거
        //Bearer 뒤 실제 토큰값을 가져옴
        String token = authorization.substring(7);
        return token;
    }
}