반응형

 

 

스프링 시큐리티를 사용하면서 직접 필터와 인증 성공, 실패 핸들러를 만들어서 설정을 하고 API 호출 시 정상적으로 인증과 인가에 대한 처리가 되는지 확인하고 있었다.

 

/api/messages 는 권한이 ROLE_MANAGER만 접근이 가능한 API이고 익명 사용자나 인증되었으나 권한이 없는 사용자는 접근할 수 없어야 한다. 실패한 경우에는 실패 사유를 JSON 형태로 전달 받도록 구성하였다.

 

/api/messages 요청을 보냈을 때, 해당 자원에 대한 적절한 스프링 시큐리티 설정이 동작하고 있다.

http.securityMatcher("/api/**")
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(auth -> auth.requestMatchers("/api/messages").hasRole("MANAGER")
            .anyRequest().authenticated()

    )
    .exceptionHandling(exceptionHandlingConfigurer -> exceptionHandlingConfigurer.authenticationEntryPoint(ajaxLoginAuthenticationEntryPoint())
            .accessDeniedHandler(ajaxAccessDeniedHandler())
    );

 

(왼쪽)인텔리제이 http client 요청. (오른쪽)  FilterChainProxy  디버깅 화면에서 직접 구현한 필터(AjaxLoginProcessingFilter)가 적용되어있다.

 

그러나 문제는 스프링 부트에서는 에러가 발생하면 /error 요청이 발생한다.

/error요청은 필터를 처음부터 다시 수행한다.

 

여기서부터 삽질했다… 이 프로젝트는 위의 스프링 시큐리티 설정과 아래의 스프링 시큐리티 설정 두 개 존재한다.

//인가 설정
http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/users", "/login**").permitAll() // login* : customAuthenticationFailureHandler의 경로 허용을 위한 처리
                .requestMatchers("/mypage").hasRole("USER")
                .requestMatchers("/api/messages").hasRole("MANAGER")
                .requestMatchers("/config").hasRole("ADMIN")
                .anyRequest().authenticated()
        )
        .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(customAccessDeniedHandler())) //접근거부시 사용되는 핸들러 등록
        .formLogin(formLoginConfigurer -> formLoginConfigurer
                .loginPage("/login")
                .loginProcessingUrl("/login_proc")
                .defaultSuccessUrl("/")
                .failureUrl("/login")
                .authenticationDetailsSource(customAuthenticationDetailsSource()) // 인증 객체에 사용자 추가 요청 정보를 저장
                .successHandler(customAuthenticationSuccessHandler()) //인증에 성공한 이후에 호출되어 동작
                .failureHandler(customAuthenticationFailureHandler()) //인증에 실패한 경우 호출되어 동작
                .permitAll()
        )
        .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());

 

/error 이라는 요청이 발생했을 때 필터를 다시 수행하고 있다. 그런데 /api/messages 요청을 했을 때와 다르게 아래의 스프링 시큐리티 설정을 사용하고 있었다.

이로 인해서 http client에서 요청한 API에서 적절한 응답이 오지 않고 해당 구성에서 사용 중인 AuthenticationFailureHandler 가 동작하여 엉뚱하게 /login 페이지를 또 요청하게 되는 현상이 발생되고 있었다.. (에러가 에러를 낳는다)

FilterChainProxy 디버깅 화면에서 처음 /api/messages를 요청했을 때와는 다른 시큐리티 설정이 사용되고 있는 것을 확인할 수 있다…

 

해결은 /error 요청일 때도 이전과 동일한 스프링 시큐리티 설정을 사용하도록 처리했다.

http.securityMatcher("/api/**", "/error")
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(auth -> auth.requestMatchers("/api/messages").hasRole("MANAGER")
						.requestMatchers("/error").permitAll()
            .anyRequest().authenticated()

    )
    .exceptionHandling(exceptionHandlingConfigurer -> exceptionHandlingConfigurer.authenticationEntryPoint(ajaxLoginAuthenticationEntryPoint())
            .accessDeniedHandler(ajaxAccessDeniedHandler())
    );

 

근본적인 문제는 요청에 대해서 필터가 여러 번 수행되기 때문에 불필요한 자원의 낭비가 발생하고 있다는 점인데 OncePerRequestFilter를 상속 받아서 구현하면 한 번의 HTTP 요청에 대해 단 한 번만 실행됨을 보장한다고 한다.

즉, 동일한 요청에 대해서는 이 필터가 여러 번 실행되지 않는 것이다.

중복으로 작업을 수행하는 것을 방지할 수 있어서 예기치 않은 동작을 방지할 수 있으니 다음에는 상황에 맞게 OncePerRequestFilter도 고려해서 사용해봐야겠다.

 

 

[참고]

AbstractAuthenticationProcessingFilter로 필터 구현 시, 쿠키 생성 안되는 현상 참고

 

반응형

+ Recent posts