▸Spring Security

스프링 필터와 스프링 시큐리티(Spring Security)의 동작 구조

코데방 2020. 4. 22.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

웹 서비스에서 스프링 시큐리티는 MVC 패턴 이전에 '필터'로서 동작합니다. 필터란 클라이언트 요청이 서블릿으로 가기 전에 먼저 처리할 수 있도록 톰캣(WAS)에서 지원해주는 기능입니다. 그래서 설정도 톰캣 설정 파일인 'web.xml' 파일에다가 해줍니다. web.xml 파일은 톰캣이 구동되는 시점에 알고 있어야 하는 정보를 넣어주는 설정입니다.

 

커스터마이징을 하기 위해서는 동작 구조를 파악해야 더 수월할 것 같아서 정리하고 넘어가는 포스팅입니다. 이번 글에서는 스프링 시큐리티가 전반적으로 동작하는 구조를 순서대로 살펴보겠습니다.

 

 


 

 

1. 톰캣(WAS)가 구동되면서 실행할 필터들의 정보를 수집

 

가장 대표적인 필터로는 인코딩 필터가 있습니다. 아래 두 가지의 필터 설정은 가장 기본적으로 들어가게 될 텐데, 클라이언트 요청을 서블릿으로 보내기 전 순서대로 실행해줍니다.

 

	<!-- 인코딩 필터 -->
	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- 스프링 Security 필터 -->
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

 

 

필터의 가장 큰 역할은 사용자 요청을 검증하고 필요에 따라 데이터를 추가하거나 변조하는 것이라고 보면 될 것 같습니다. 예를 들어 인코딩 필터는 사용자가 요청한 내용을 담은 request 객체의 정보를 특정 인코딩 타입으로 중간에 바꿔줍니다. 스프링 시큐리티의 인터셉터는 특정 페이지(URL)에 대한 요청에 대해 권한이 없을 경우 로그인 페이지의 URL로 바꿔서 서블릿에게 전달해줍니다. request 객체의 요청 내용을 변조하는 것이죠. 그밖에도 단순히 사용자 요청 전/후로 처리해야할 로직을 수행하게 할 수도 있습니다.

 

스프링 시큐리티를 적용하기 위해 등록하는 필터 클래스는 위 설정에 보이는 딱 하나입니다. 가장 최초로 실행되는 스프링의 필터 클래스이고, 스프링 Security의 필터 클래스들을 필터 체인에 등록해 사용할 수 있도록 해줍니다. 중간 한 지점에서 대표로 받아준다는 의미에서 '프록시'라는 단어가 이름에 붙어있습니다. 가장 코어 기능이라 아마 이 부분까지 커스터마이징할 일은 거의 없지 않을까 합니다.

 

 

 


 

 

2. 톰캣(WAS)에서 필터 클래스의 객체 생성 후 doFilter() 메소드 호출

 

톰캣은 등록된 필터들의 클래스를 모두 객체화해서 내부 저장합니다. 필터가 초기화될 때 필요한 부분들 또한 마찬가지입니다. 이 객체들은 설정된 대로 순서를 가지게 되는데, 이 순서 정보를 가진 "FilterChain" 인터페이스 객체를 메소드의 파라미터로 넘겨줍니다. 실제 구현체는 아래 클래스입니다. 필터들을 줄줄이 엮어놓았다고 해서 '체인'이라고 부르는것 아닐까 합니다.

 

모든 필터 클래스는 "Filter" 인터페이스를 상속받아야 하고, 구현해야 하는 세 개의 메소드 중 실제 필터의 기능을 담당하는 doFilter 메소드를 구현할 때 아래와 같이 FilterChain 객체를 넘겨주게 됩니다.

 

* FilterChain 구현체 : class org.apache.catalina.core.ApplicationFilterChain

public void doFilter
	( ServletRequest request, ServletResponse response, FilterChain chain ) 
    throws IOException, ServletException;

 

FilterChain 인터페이스에도 doFilter()라는 메소드가 있습니다. 이는 일반 필터들이 상속받는 Filter 인터페이스의 doFilter()와 다른 메소드입니다. FilterChain의 doFilter()는 다음 차례 필터 클래스 객체의 doFilter() 메소드를 호출시키는 기능을 합니다. 만약 더 이상 실행될 필터가 없다면 필터들에서 이리저리 만져둔 request 객체와 response 객체를 서블릿으로 넘겨 처리하게 됩니다.

 

 

 

따라서 필터 클래스를 직접 만들어 사용할 때 만약 filterChain.doFilter() 메소드를 호출해주지 않으면 다음 필터로 넘어가지도 않을 뿐더러 서블릿으로 요청이 가지도 못한다는 것에 주의해야 합니다.

 

 

 


 

 

3. 스프링 SecurityDelegatingFilterProxy 클래스에서 스프링 시큐리티의 필터 클래스 등록

 

스프링에서 제공하는 SecurityDelegatingFilterProxy 클래스는 스프링 시큐리티의 기능별 필터 클래스를 호출해주는 역할을 합니다. 스프링과 스프링 시큐리티의 접합점이라고 보면 될 것 같습니다.

 

정확히 내부 로직을 모두 파보진 않았지만 대충 훑어보면 컨텍스트 설정 파일을 참조해서 필터 클래스들(그리고 필요한 추가 클래스들)을 Bean으로 만들고, 위에서 언급한 FilterChain에 필터 클래스들을 추가해둔 뒤 가장 첫번 째 필터 클래스를 호출하는 것 같습니다.

 

중요한건 정해진 순서대로 스프링 Security의 필터들이 모두 FilterChain에 엮이게 되고, 각자 로직을 수행하고 나면 filterChain.doFilter() 메소드를 호출해 다음 필터 클래스의 doFilter() 메소드를 실행시킨다는 것입니다.

 

 

 

실행된 필터 클래스의 doFilter() 메소드는 각각의 역할을 맡고 있는 다른 클래스들을 호출해서 자신의 일을 마무리한 뒤 다음 필터를 실행해줍니다. 모든 doFilter() 메소드는 void 이기 때문에 리턴값 없이 독립적으로 수행됩니다. 이 말은 데이터 전달은 request나 session, 또는 전역적인 Context(컨테이너)에 의해 이루어지며 필터 간의 상호 의존관계는 없다는 의미입니다.

 

 


 

 

4. 스프링 Security 필터 클래스가 순서대로 실행

 

커스터마이징을 제대로 하기 위해서는 이 과정을 잘 이해해야할 것 같습니다. 기본 제공되는 필터 클래스는 약 20개가 있는데, 무조건 로드되는 필터도 있고 컨텍스트 설정에 따라 로드되는 필터도 있습니다. 그리고 각각의 필터 클래스는 doFilter() 메소드가 실행될 때 가장 먼저 자신의 역할을 실행할 조건인지를 먼저 체크하게 됩니다.

 

독립적으로 움직이는 필터도 있겠지만 이전 필터에서 해둔 작업 내용을 보고 판단하는 경우도 있기 때문에, 커스터마이징할 때는 해당 필터가 동작하는 조건에 대해 신경을 써줘야 합니다. 필터의 내용이 이후 실행되는 다른 필터에게 영향을 미칠 수 있기 때문입니다. 그래서 웬만하면 기존에 짜여진 코드의 구조를 동일하게 맞춰주면서 필요한 부분만 손을 보는 것이 가장 안전합니다.

 

예를 들어 RememberMeAuthenticationFilter 클래스는 Remember-me 기능을 수행하는 필터 클래스입니다. 이 클래스의 doFilter() 메소드의 시작은 아래와 같습니다. 

 

   if (SecurityContextHolder.getContext().getAuthentication() == null) 

 

위 코드는 해당 세션에 저장된 Authentication 인증객체가 비어있다면 로직을 전개하겠다는 의미입니다. 이 객체가 이미 세션에 포함되어 있으면 이미 인증을 받아 로그인된 상태라는 것이므로 Remember-me가 동작할 이유가 없는 것이죠. 

 

아래는 Spring Security docs에 나와있는 필터 클래스의 목록입니다. 위에서 아래 순서대로 동작하게 됩니다. 종류가 너무 많으니 필요한 기능을 커스터마이징할 때마다 하나씩 다뤄보도록 하겠습니다. 

 

 

 


 

 

5. 필터 클래스는 각 모듈 별 기능을 가진 객체(클래스)의 메소드를 호출해서 로직 전개

 

필터 클래스와 기능을 수행하는 기타 클래스들이 있습니다. 구조를 보면 깔끔하게 분리되어 있지 않고 이리저리 엮여있는 구조입니다. 예를 들어 Persistent Remember-me 기능을 구현하는 클래스에는 실제 필터 클래스에서 호출하는 메소드 4개가 있습니다. 하지만 이 메소드를 모두 Remember-me의 필터가 호출하는 것이 아니라 각각 필요한 필터 클래스에서 호출해서 사용합니다.

 

따라서 각 커스터마이징할 때 필수 메소드들을 오버라이딩하려면 어디서 왜 호출하는지, 호출 후 어떻게 처리하는지를 확인하고 수정해야 합니다. 본래 코드의 주석에 어느정도 설명이 나와 있습니다. 

 

 

 


 

 

스프링 필터를 통해 스프링 시큐리티가 구동되는 전반적인 흐름에 관해 살펴보았습니다. 아무래도 커스터마이징을 깊게 하려면 구조에 대해 이해하고 있어야 할 것 같습니다. 코드를 따라가보면서 구조를 그려본거라 틀린 부분이 있을 수 있는데 혹시 틀린 부분이 있으면 지적해주시면 감사하겠습니다. 

728x90

댓글

💲 추천 글