Security Filter 기능

패스트캠퍼스 Spring Security 강의 정리

Security Filter

  • Spring security의 동작은 사실상 filter로 동작
  • 각자 다른 기능을 가진 다양한 필터 존재
  • 필터는 제외하거나 추가 가능
  • 필터의 동작 순서 변경 가능
  • 대표적인 필터
    • SecurityContextPersistenceFilter
    • BasicAuthenticationFilter
    • UsernamePasswordAuthenticationFilter
    • CsrfFilter
    • RememberMeAuthenticationFilter
    • AnonymousAuthenticationFilter
    • FilterSecurityInterceptor
    • ExceptionTranslationFilter
  • 각 필터는 GenericFilterBean을 상속하고 GenericFilterBean은 Filter를 구현함
public abstract class OncePerRequestFilter extends GenericFilterBean 
  
public abstract class GenericFilterBean implements Filter
  • 필터는 요청이나 응답에 작업을 수행
  • doFilter 메소드에서 필터링 작업을 하며 필터는 doFilter를 구현해야 함
public interface Filter {
  public default void init(FilterConfig filterConfig) throws ServletException {}
  
  public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
  
  public default void destroy() {}
}
  • FilterChainProxy
    • doFilterInternal에서 적용된 필터의 개수와 동작 순서 확인 가능
  • FilterOrderRegistration
    • 필터 순서 정의
    • 100번부터 시작해서 100씩 증가됨
    • 100씩 증가되는 사이에 커스텀 필터 추가 가능
// spring security v5.7.2
final class FilterOrderRegistration {

	private static final int INITIAL_ORDER = 100;

	private static final int ORDER_STEP = 100;

	private final Map<String, Integer> filterToOrder = new HashMap<>();

	FilterOrderRegistration() {
		Step order = new Step(INITIAL_ORDER, ORDER_STEP);
		put(DisableEncodeUrlFilter.class, order.next());
		put(ForceEagerSessionCreationFilter.class, order.next());
		put(ChannelProcessingFilter.class, order.next());
		order.next(); // gh-8105
    ...
  }

SecurityContextPersistenceFilter

  • SecurityContext를 찾아서 SecurityContextHolder에 넣어주는 역할
  • 기본적으로 SecurityContext는 HttpSession에서 가져옴(HttpSessionSecurityContextRepository)
    • 로그인 하지 않아도 JSESSIONID로 로그인 상태를 유지할 수 있도록 함
  • SecurityContext가 없으면 새로 생성
  • 5.7.2부터 deprecated 되었으며 SecurityContextHolderFilter 사용
public class SecurityContextPersistenceFilter extends GenericFilterBean {

  private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    
    ...
    // security context 불러오기
    SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
      // security context holder에 security context 넣어주기
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}
			chain.doFilter(holder.getRequest(), holder.getResponse()); // 다음 필터 실행
		}      ...
  
}

BasicAuthenticationFilter

  • 로그인 과정을 거치지 않아도 요청 가능
  • 로그인 데이터를 Base64로 인코딩해서 모든 요청에 포함해서 보내서 인증
    • username:password 데이터 인코딩
    • Authorization 헤더에 Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • 세션이 필요없고 요청할 때마다 인증 과정(stateless)
  • 보안에 취약하므로 https 사용
  • 사용하지 않을 때는 http.httpBasic().disable() 비활성화 처리

UsernamePasswordAuthenticationFilter

  • form 데이터로 username, password 기반의 인증
  • 흐름
    • UsernamePasswordAuthenticationFilter 통해 인증 시도
    • ProviderManager(AuthenticationManager) 인증 정보 제공
    • AbstractUserDetailsAuthenticationProvider 사용자 계정의 상태나 비밀번호 일치 여부 등 확인
    • DaoAuthenticationProvider 사용자 정보 제공
    • UserDetailsService 사용자 정보 제공하는 service
// UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
    ...
      
    // this.getAuthenticationManager() -> ProviderManager
    return this.getAuthenticationManager().authenticate(authRequest);
  }

}

// ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
  
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
    try {
      	// provider -> AbstractUserDetailsAuthenticationProvider
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}  
    ...
    
  } 
}

// AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
  
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
    try {
      	// retrieveUser -> DaoAuthenticationProvider
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
    ...
  }  
}

// DaoAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  
  protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    prepareTimingAttackProtection();
		try {
      // this.getUserDetailsService() -> UserDetailsService
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      ...
    }
  }  
}

CsrfFilter

  • CSRF 공격을 방어하는 필터
    • 사이트 간 요청 위조
    • 사용자 의지와 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격
  • CSRF 토큰으로 요청 확인
  • 정상적인 페이지는 CSRF 토큰이 없거나 잘못된 토큰을 가짐
  • 자동으로 활성화되어 있는 필터
  • 명시적으로 실행하기 위해 http.csrf()
  • 비활성화할 때는 http.csrf().disable()

RememberMeAuthenticationFilter

  • 일반 세션보다 더 긴 시간 로그인 사실을 기억하도록 함
  • 세션의 기본 만료 시간은 30분
  • RememberMeAuthenticationFilter 기본 만료 시간은 2주
  • RememberMe 토큰으로 인증
  • 서버가 꺼지지 않으면 브라우저를 껐다 켜도 세션이 유지됨
  • 활성화 http.rememberMe()

AnonymousAuthenticationFilter

  • 인증 안 된 사용자가 요청할 때 익명 유저로 만들어 authentication에 넣어주는 필터
  • 다른 필터에서 익명 유저인지 확인 가능
  • AnonymousAuthenticationToken
  • 활성화 http.anonymous()
  • principal("anonymousUser")로 principal 변경 가능

FilterSecurityInterceptor

  • 앞의 필터를 통해 넘어온 authentication을 기반으로 최종 인가 판단
  • 보통 필터 중에서 뒤쪽 순서
  • 인증을 가져오고 문제가 있으면 AuthenticationException 발생
  • 문제가 없으면 해당 인증으로 인가 판단
  • 인가가 거절되면 AccessDeniedExcetion 발생
  • 승인되면 정상적으로 필터 종료
  • 흐름
    • FilterSecurityInterceptor.doFilter()
    • AbstractSecurityInterceptor.beforeInvocation()
    • AbstractSecurityInterceptor.authenticateIfRequired()
      • 인증에 문제 있을 경우 AuthenticationException
    • AbstractSecurityInterceptor.attemptAuthorization()
      • 인가에 문제 있을 경우 AccessDeniedExcetion

ExceptionTranslationFilter

  • 예외를 처리해주는 필터
    • AuthenticationException
    • AccessDeniedException
  • handleSpringSecurityException에서 처리
    • handleAuthenticationException() & handleAccessDeniedException() 분기 처리