[Spring] 서블릿 필터를 이용한 권한 검사

김강욱·2024년 6월 3일
0

Project-Evertrip

목록 보기
18/19
post-thumbnail

이번 포스팅에서는 서블릿 필터를 이용한 권한 검사를 구현해보려고 합니다.

🎈 Why 서블릿 필터?

현재 진행중인 프로젝트에서 게시글 작성자 본인만 볼 수 있는 게시글 이벤트 통계 페이지가 있습니다. 해당 페이지에 접근할 때 해당 사용자가 게시글 작성자인지 확인한 후 아닐 시 요청을 거절해야하는데, 이렇게 공통적으로 인증 또는 권한 검사를 하는데 유용하게 사용할 수 있는 것이 바로 서블릿 필터입니다.

물론 이러한 공통 관심사를 처리하는데 스프링의 AOP를 사용하거나 스프링 인터셉터를 사용할 수 있지만 스프링 시큐리티 필터에서 사용자 인증을 검사한 다음으로 권한 검사를 간단하게 할 수 있도록 서블릿 필터를 사용하였습니다.


💡 서블릿 필터 적용하기

이제 서블릿 필터를 생성하여 권한 검사에 적용해보도록 하겠습니다. 우선 필터 인터페이스를 구현하는 커스텀 필터를 생성해보도록 하겠습니다.

⚙️ PostLogFilter 생성

@Slf4j
public class PostLogFilter implements Filter {

    private PostRepository postRepository;

    public PostLogFilter(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("PostLogFilter init");
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return;
        } else {
            Long memberId = Long.parseLong(authentication.getName());

            String requestURI = httpRequest.getRequestURI();

            Pattern pattern = Pattern.compile("/api/post-logs/(\\d+)/(\\w+)");
            Matcher matcher = pattern.matcher(requestURI);

            try {
                if (matcher.find()) {
                    Long postId = Long.parseLong(matcher.group(1));
                    postRepository.getPostByPostIdAndMemberId(memberId, postId).orElseThrow(() -> new ApplicationException(ErrorCode.NOT_WRITER));
                    chain.doFilter(request,response);
                } else {
                    log.info("알맞지 않은 URL 요청", requestURI);
                    throw new ApplicationException(ErrorCode.URL_NOT_FOUND);
                }
            } catch (ApplicationException e) {
                sendErrorResponse(httpResponse, e);
                return;
            }



        }
    }

    private void sendErrorResponse(HttpServletResponse response, ApplicationException e) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        String jsonInString = mapper.writeValueAsString(ApiResponse.error(ErrorResponse.of(e.getErrorCode())));

        response.setContentType("application/json");
        response.setStatus(e.getErrorCode().getStatus().value());
        response.getWriter().write(jsonInString);
    }

    @Override
    public void destroy() {
        log.info("PostLogFilter destroy");
        Filter.super.destroy();
    }

}

필터 인터페이스에서 오버라이딩하는 메서드는 3개입니다.
init() 메서드는 필터 초기화 메서드이며 서블릿 컨테이너가 생성될 때 호출됩니다. doFilter() 메서드는 고객의 요청이 올 때 마다 해당 메서드가 호출됩니다. 해당 메서드에 필터의 로직을 구현하면 됩니다. destroy() 메서드는 필터 종료 메서드로 서블릿 컨테이너가 종료될 때 호출됩니다.

doFilter() 메서드를 살펴보도록 하겠습니다.

먼저 파라미터로 전달받는 ServletRequest, ServletResponse 객체를 URL 정보를 읽어들이거나 HTTP 응답을 반환해주기 위해 HttpServletRequest, HttpServletResponse로 다운캐스팅해줍니다.

또한 스프링 시큐리티 필터에서 사용자 인증이 정상적으로 통과될 시 생성해준 Authentication 객체를 받아와 사용자 정보(여기서는 사용자 pk)를 확인하게 됩니다.

HTTP 요청에서 URI를 가져와 정규 표현식을 이용하여 /api/post-logs/로 시작하고, 그 뒤에 하나 이상의 숫자, 그리고 하나 이상의 알파벳 문자가 오는 문자열인지 패턴을 확인하게 됩니다.

패턴이 일치하지 않을 시 알맞지 않은 URL 요청이라 로그를 출력하고 404 에러를 반환하게 됩니다. 패턴이 일치할 시 DB를 조회하여 해당 사용자가 게시글 작성자가 맞는지 확인하게 됩니다.

doFilter() 메서드에서 return; 을 하게 될 시 다음 필터를 통과하지 않고 WAS 서버단으로 반환이 되며 chain.doFilter(request,response);를 통해 다음 필터로 넘어갈 수 있습니다.

이제 생성한 PostLogFilter를 필터 체인에 등록해보도록 하겠습니다. 서블릿 컨테이너는 필터 체인에 등록된 해당 필터를 싱글톤 객체로 생성하고, 관리하게 됩니다.

⚙️ WebConfig 설정

@Configuration
@RequiredArgsConstructor
public class WebConfig {

    private final PostRepository postRepository;

    @Bean
    public FilterRegistrationBean postLogFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new
                FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new PostLogFilter(postRepository));
        filterRegistrationBean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER+1);
        filterRegistrationBean.addUrlPatterns("/api/post-logs/*");
        return filterRegistrationBean;
    }
}

FilterRegistrationBean를 사용해서 필터 체인에 등록해주었습니다. setFilter()는 등록할 필터를 지정해주고, setOrder()는 필터 체인 내에서 순서를 지정해줍니다. addUrlPatterns()는 필터를 적용할 URL 패턴을 지정해주게 됩니다.

setFilter()에 위에서 생성한 PostLogFilter 객체를 생성하여 넣어주고, setOrder()에 스프링 시큐리티 필터의 순서 다음으로 지정해줍니다. addUrlPatterns()에 URL의 공통 부분을 기입해줍니다.


📝 테스트 해보기

이제 테스트를 진행해보도록 하겠습니다. 먼저 스프링 시큐리티 필터가 동작한 다음 게시글 작성자에 대한 권한 검사가 이뤄지는지 테스트하고 알맞지 않는 URL 패턴 및 게시글 작성자 본인이 아닌 경우의 요청에 대해 테스트해보겠습니다.

1. 스프링 시큐리티 필터 동작 다음 권한 검사 여부

해당 요청을 토큰을 빼고 요청해보니 다음과 같이 정상적으로 인증 에러가 반환된 것을 확인할 수 있었습니다.



2. 알맞지 않은 URL 패턴

/api/post-logs URL의 뒤에 알맞지 않은 URL 패턴의 요청을 보낸 결과

필터에서 설정해둔 알맞지 않은 URL 요청 로그가 출력되고 404 에러를 반환하는 것을 확인할 수 있었습니다.



3. 게시글 작성자 본인이 아닐 경우

Authorization 토큰의 사용자는 게시글 작성자가 아니며 해당 토큰으로 요청을 보낸 결과

정상적으로 403 Forbidden 에러를 반환하는 것을 확인할 수 있었습니다.



4. 게시글 작성자일 경우

업로드중..

정상적인 응답을 받는 것을 확인할 수 있었습니다.

profile
TO BE DEVELOPER

0개의 댓글