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는 클라이언트 요청이 서블릿에 닿기 전에 전처리하고,
서버에서 응답을 클라이언트에 보내기 전 후처리할 때 사용되는 객체에요.
Spring MVC와 같이 사용한다면 위 그림처럼 DispatcherServlet에 도달하기전에 처리를 하겠지요.
필터체인은 서블릿 컨테이너가 관리하는 ApplicationFilterChain으로, 클라이언트가 요청을 보내면 서블릿 컨테이너에서 URI를 확인 후 필터와 서블릿을 매핑합니다. Spring security에서 사용하는 필터체인은 DelegatingFilterProxy를 사용합니다.
그렇다면 클라이언트 요청에 전처리/후처리가 왜 필요하며, 필터에서 따로 처리할까요?
바로 비지니스로직과 관심사의 분리 때문이에요.
백엔드에서 비지니스 로직의 구현에 집중을 하고, 비지니스 로직과 관련이 덜한 로직은 필터에 위임시키는 거에요.
로그인으로 예를 들자면
비지니스로직: 아이디가 db에 존재하는지, (encoding된)패스워드가 맞는지 확인
필터: http 요청이 적절한 사용자로부터 왔는지 인증/인가
뿐만 아니라 필터는 비지니스 로직과 무관한 암호화, 로깅 등을 할 수 있어요.
위 그림은 앞서 보았던 그림에서 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
스프링부트 핵심가이드