Spring Security (2) 아키텍처

허진혁·2022년 12월 7일
0
post-thumbnail

Spring security는 서블릿 필터(Servlet Filter)를 기반으로 동작해요.

만약 서블릿이라는 단어가 낯설다면 아래 링크를 참고해주세요 🙇

https://velog.io/@wlsgur1533/Servlet%EC%9D%84-%EA%B3%A0%EB%AF%BC%ED%95%B4%EB%B3%B4%EC%9E%90

Servlet Filter

Servlet Filter는 클라이언트 요청이 서블릿에 닿기 전에 전처리하고,
서버에서 응답을 클라이언트에 보내기 전 후처리할 때 사용되는 객체에요.

Spring MVC와 같이 사용한다면 위 그림처럼 DispatcherServlet에 도달하기전에 처리를 하겠지요.

필터체인은 서블릿 컨테이너가 관리하는 ApplicationFilterChain으로, 클라이언트가 요청을 보내면 서블릿 컨테이너에서 URI를 확인 후 필터와 서블릿을 매핑합니다. Spring security에서 사용하는 필터체인은 DelegatingFilterProxy를 사용합니다.

그렇다면 클라이언트 요청에 전처리/후처리가 왜 필요하며, 필터에서 따로 처리할까요?

바로 비지니스로직과 관심사의 분리 때문이에요.
백엔드에서 비지니스 로직의 구현에 집중을 하고, 비지니스 로직과 관련이 덜한 로직은 필터에 위임시키는 거에요.

로그인으로 예를 들자면

비지니스로직: 아이디가 db에 존재하는지, (encoding된)패스워드가 맞는지 확인
필터: http 요청이 적절한 사용자로부터 왔는지 인증/인가

뿐만 아니라 필터는 비지니스 로직과 무관한 암호화, 로깅 등을 할 수 있어요.

Spring security 아키텍처

위 그림은 앞서 보았던 그림에서 DelegatingFilterProxy가 추가된 것을 확인할 수 있어요.

DelegatingFilterProxy는 서블릿 컨테이너와 스프링 컨테이너(ApplicationContext) 사이에 다리 역할을 수행하는 필터 구현체에요. DelegatingFilterProxy는 요청을 ApplicationContext에 있는 FilterChainProxy에게 위임하게 되요.

그럼 FilterChainProxy에 등록된 보안필터체인(SecurityFilterChain)을 통해 많은 보안필터 처리가 이루어져요. FilterChainProxy에서 사용할 수 있는 보안 필터 체인은 List형식으로 담을 수 있게 설정되어 있어 URI 패턴에 따라 특정한 SecurityFilterChain을 선택해서 사용할 수 있어요.

아래는 FilterChainProxy에 정의된 doFilter에요.

@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (!clearContext) {
            doFilterInternal(request, response, chain);
            return;
        }
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            doFilterInternal(request, response, chain);
        }
        catch (RequestRejectedException ex) {
            this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
        }
        finally {
            SecurityContextHolder.clearContext();
            request.removeAttribute(FILTER_APPLIED);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
        List<Filter> filters = getFilters(firewallRequest);
        if (filters == null || filters.size() == 0) {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
            }
            firewallRequest.reset();
            chain.doFilter(firewallRequest, firewallResponse);
            return;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
        }
        VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
        virtualFilterChain.doFilter(firewallRequest, firewallResponse);
    }

    private List<Filter> getFilters(HttpServletRequest request) {
        int count = 0;
        for (SecurityFilterChain chain : this.filterChains) {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
                    this.filterChains.size()));
            }
            if (chain.matches(request)) {
                return chain.getFilters();
            }
        }
        return null;
    }

doFilter()를 봐보죠. doFilterInternal() 함수에 필터 처리를 넘겨주는데요.

이 과정에서 List<>를 가져오네요.

FilterChainProxy은 스프링부트의 자동 설정에 의해 자동 생성이 되고,
SecurityFilterChain은 ApplicationContext의 관리를 받게 되는 Bean으로 등록이 되요.

모든 보안필터가 처리가 된다면 비로소 Spring MVC로 요청이 넘어가요.

Spring Security는 인증/인가와 관련된 여러 개의 Filter를 제공해주고 있어요. 특히, 세션 기반의 로그인 방식에 대해서 정말 많은 지원을 해줘요.

참고
https://docs.spring.io/spring-security/reference/servlet/architecture.html
스프링부트 핵심가이드

profile
Don't ever say it's over if I'm breathing

0개의 댓글