본문 바로가기
스프링 부트/시큐리티

[스프링 시큐리티] 주요 Security Filter들 정리

by illlilillil 2022. 5. 24.

ChannelProcessingFilter

전송 레이어 보안을 위해 SSL(TLS) 인증서를 생성하고 어플리케이션에 저장한다.

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
        Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(filterInvocation);
        if (attributes != null) {
            this.logger.debug(LogMessage.format("Request: %s; ConfigAttributes: %s", filterInvocation, attributes));
            this.channelDecisionManager.decide(filterInvocation, attributes);
            if (filterInvocation.getResponse().isCommitted()) {
                return;
            }
        }

        chain.doFilter(request, response);
    }

SecurityContextPersistenceFilter

인증 필터 관련해서 최상단에 위치한다. 인증된 사용자는 재로그인이 필요없도록 한다.

SecurityContext가 존재하지 않으면 빈 SecurityContext를 생성한다.

SecurityContextRepository(인터페이스)를 통해 SecurityContext를 영속화를 진행하는 필터이다.

기본 설정은 HttpSessionSecurityContextRepository 구현체를 통해 HttpSession의 attributes에 SecurityContext가 저장된다.

 

AbstractAuthenticationProcessingFilter

사용자 인증을 처리하기 위한 필터 인터페이스로 대표적인 구현체는 UsernamePasswordAuthenticationFilter 구현체가 있다.

UsernamePasswordAuthenticationFilter

인증이 필요한 사용자가 접근할 경우 인증에 성공하면 UsernamePasswordAuthenticationToken 객체를 Holder에 저장한다.

최종 인증이 되면 SecurityContextPersistenceFilter가 Session에 SecurityContext를 저장한다.

 

기본 필드는 다음과 같다

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

UsernamePasswordAuthenticationToken

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 560L;
    private final Object principal; //아이디
    private Object credentials; //비밀번호
}

AnonymousAuthenticationFilter

익명 사용자라면 AnonymousAuthenticationToken 객체를 생성해 SecurityContext에 저장한다.

    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
    private String key; 
    private Object principal; //anonymousUser
    private List<GrantedAuthority> authorities; //ROLE_ANONYMOUS

doFilter에서 SecurityContext의 인증 정보를 조회하고 없으면 새 인증 객체를 생성하고 주입시킨다.

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        Authentication authentication = this.createAuthentication((HttpServletRequest)req);
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.of(() -> {
                return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
            }));
        } else {
            this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
        }
    } else if (this.logger.isTraceEnabled()) {
        this.logger.trace(LogMessage.of(() -> {
            return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
        }));
    }

    chain.doFilter(req, res);
}

RememberMeAuthenticationFilter

동작 조건은 헤더에 remember-me가 있는 경우나 인증 객체가 SecurityContext에 존재하지 않는 경우

Token 기반의 구현 방법과 DB 기반의 구현 방법 두가지가 있다. 기본 Default는 Token 기반 구현이다.

  • remember-me 기반 인증과 로그인 아이디/비밀번호 기반 인증 결과가 명백히 다르다.
    • remember-me 기반 인증 결과는 RememberMeAuthenticationToken
    • 로그인 아이디/비밀번호 기반 인증 결과는 UsernamePasswordAuthenticationToken

isFullyAuthenticated 메소드로 로그인 아이디/비밀번호로 인증한 사용자만 접근할 수 있다.

public void init(H http) throws Exception {
    this.validateInput();
    String key = this.getKey();
    RememberMeServices rememberMeServices = this.getRememberMeServices(http, key);
    http.setSharedObject(RememberMeServices.class, rememberMeServices);
    LogoutConfigurer<H> logoutConfigurer = (LogoutConfigurer)http.getConfigurer(LogoutConfigurer.class);
    if (logoutConfigurer != null && this.logoutHandler != null) {
        logoutConfigurer.addLogoutHandler(this.logoutHandler);
    }

    RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(key);
    authenticationProvider = (RememberMeAuthenticationProvider)this.postProcess(authenticationProvider);
    http.authenticationProvider(authenticationProvider);
    this.initDefaultLoginFilter(http);
}

두 가지 구현체가 있어 사용자는 필요에 따라 커스텀할 수 있다.

  • TokenBasedRememberMeServices — MD5 해시 알고리즘 기반 쿠키 검증
  • PersistentTokenBasedRememberMeServices — 외부 데이터베이스에서 인증에 필요한 데이터를 가져오고 검증한다. 사용자마다 고유의 Series 식별자가 생성되고, 인증 시 마다 매번 갱신되는 임의의 토큰 값을 사용하여 보다 높은 보안성을 제공한다.
public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
    public TokenBasedRememberMeServices(String key, UserDetailsService userDetailsService) {
        super(key, userDetailsService);
    }
}
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    private SecureRandom random = new SecureRandom();
    public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService);
        this.tokenRepository = tokenRepository;
    }
}

 

ExceptionTranslationFilter

FilterSecurityInterceptor 바로 위에 위치하고 FilterSecurityInterceptor 실행 중 발생할 수 있는 예외를 잡고 처리한다.

따라서 커스텀 필터를 추가할때 ExceptionTranslationFilter의 위치를 잘 생각해서 두어야 예외를 잡을 수 있다.

FilterSecurityInterceptor 실행 중 발생할 수 있는 예외 처리를 담당한다.

 

 

Try - Catch를 통해 FilterSecurityInterceptor의 예외를 처리한다.

securityException에 아래 두 개의 Exception의 내용이 담기게 된다.

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (securityException == null) {
                this.rethrow(var8);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }
  • AuthenticationException - 인증 관련 예외, 사용자를 로그인 페이지로 보낸다. AuthenticationEntryPoint를 이용해 처리
  • AccessDeniedException - AccessDecisionManager에 의해 접근 거부가 발생했을 때 접근 거부 페이지를 보여주거나 사용자를 로그인 페이지로 보낸다. AccessDeniedHandler를 사용하여 처리

[추가 내용] AuthenticationEntryPoint, AccessDeniedHandler란?

AuthenticationEntryPoint

인증되지 않은 사용자 요청을 처리할때 로그인 요청 페이지로 보내는 역할을 한다.

AccessDeniedHandler

기본 에러 페이지를 보여주는 역할을 한다.

FilterSecurityInterceptor

시큐리티 필터 체인 상에서 가장 마지막에 위치한다. 사용자의 권한과 리소스에서 요구하는 권한을 취합해 접근을 허용할지 결정한다.

실질적인 판단은 AccessDecisionManager에서 수행한다. 해당 필터가 호출될 때는 사용자는 이미 인증이 완료됨 시점이다. Authentication 인터페이스의 getAuthorities() 메소드를 통해 인증된 사용자의 권한 목록을 가져올수 있다. 익명 사용자도 인증이 완료된 것으로 간주하며, ROLE_ANONYMOUS 권한을 갖는다.

WebAsyncManagerIntegrationFilter

SecurityContextHolder는 ThreadLocal 기반 동작이다. 같은 Thread에서만 SecurityContext를 공유할 수 있다.

WebAsyncManagerIntegrationFilter는 Async 관련 기능 사용 시 SecurityContext를 공유할 수 있도록 돕는다.

 

CsrfFilter

CsrfFilter는 원하지 않는 요청을 악의적으로 만들어 보내는 기법을 막는 필터이다.

아래 2가지 조건으로 CSRF를 통해 사용자 권한을 도용할 수 있다.

  • 위조 요청을 전송하는 서비스에 사용자가 로그인 상태
  • 사용자가 해커가 만든 피싱 사이트에 접속

방지법

  1. Referrer 검증 - Request의 referrer를 확인하여 domain이 일치 - CORS는 도메인이 달라도 허용하기 때문에 조심해서 사용해야 한다.
  2. CSRF Token(기본 활성) -리소스 변경 요청 시 토큰 여부를 확인해 공격 방지, 요청마다 세션에 임의의 토큰 값을 저장하고 전송한다. 리소스 변경 요청 시 저장된 토큰 값과 요청 파라미터의 토큰 값 일치를 확인한다. CsrfTokenRepository는 토큰 저장소 인터페이스이며 HttpSessionCsrfTokenRepository가 기본 구현체로 되어 있다.

HeaderWriterFilter

응답 헤더에 보안 관련 헤더를 추가하고 MIME sniffing, XSS, clickjacking 등을 방어한다.

BasicAuthenticationFilter

Base64 인코딩 처리, Basic 인증을 처리, HTTPS 프로토콜에서만 제한적으로 사용 권장(자주 사용 하지 않는다.)

 

WebAsyncManagerIntegrationFilter

기본적으로 SecurityContext는 스레드 내에서만 공유할 수 있다. 그러나 WebAsyncManagerIntegrationFilter를 통해 인증 정보를 공유할 수 있다. MVC의 비동기 요청 처리만 적용할 수 있다.(@Controller)

SecurityContextHolderStrategy의 기본 설정은 SecurityContextHolder.MODE_THREADLOCAL이다.

SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 설정을 통해 타 쓰레드에서도 SecurityContext를 참조할 수 있게된다.

public WebSecurityConfigure() {
  SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
@Controller
public class PostController {
	... ...
  @GetMapping(path = "/post")
  @ResponseBody
  public String getPosts() {
    postService.asyncMethod();
    return "OK";
  }
}

@Service
public class PostService {

  @Async
  public String asyncMethod() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    User principal = authentication != null ? (User) authentication.getPrincipal() : null;
    String name = principal != null ? principal.getUsername() : null;
    return name;
  }

}

 

RequestCacheAwareFilter

인증 요청에 의해 가로채어진 원래 요청으로 이동하게 해주는 필터이다.

익명 사용자가 인증 권한이 필요한 페이지에 접근할때 AccessDecisionManager에 의해 접근 거부가 일어나게 된다. 

그 후 로그인 페이지에 이동하게 되는데 로그인 후에 원래 요청했던 페이지로 갈 수 있도록 하는 역할을 한다.

캐시된 요청이 있으면 캐시된 요청을 처리하고 없으면 현재의 요청을 처리하는 로직이다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest)request, (HttpServletResponse)response);
        chain.doFilter((ServletRequest)(wrappedSavedRequest != null ? wrappedSavedRequest : request), response);
}

SecurityContextHolderAwareRequestFilter

 

 

DefaultLoginPageGeneratingFilter

HTTP GET 요청에 대해 디폴트 로그인 페이지를 생성해준다.

SessionManagementFilter

session-fixation attack - 정상 사용자의 세션을 탈취해 인증을 우회하는 기법이다.

이러한 문제를 해결하기 위해 세션 고정 보호 방식으로 사용자를 보호한다.

인증 전 발급 받은 세션 ID가 인증  후에도 같으면 문제가 될 수 있다.

인증 전에 사용자가 가지고 있던 세션을 인증 후에는 사용하지 않도록 해 위의 공격을 방어한다.

 

스프링 시큐리티는 4가지 설정을 제공한다.

  • none — 세션을 그대로 유지
  • newSession — 새로운 세션을 만들고, 기존 데이터는 복제하지 않는다.
  • migrateSession — 새로운 세션을 만들고, 데이터를 모두 복제한다.
  • changeSession — 새로운 세션을 만들지 않고, session-fixation 공격을 방어함 (servlet 3.1 이상)

 

  • 세션 생성 전략 설정은 네 가지를 제공한다.
    • IF_REQUIRED — 필요시 생성함 (default)
    • NEVER — Spring Security에서는 세션을 생성하지 않지만, 세션이 존재하면 사용함
    • STATELESS — 세션을 완전히 사용하지 않음 (JWT 인증이 사용되는 REST API 서비스에 적합)
    • ALWAYS — 항상 세션을 사용함
  • 동일 사용자의 중복 로그인 감지와 처리는 다음 메서드에서 담당
    • maximumSessions - 동일 사용자의 최대 동시 세션 개수
    • maxSessionsPreventsLogin - 최대 개수 초과 시 인증 시도 차단 여부(default = false)
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            ... ... ...
            .and()
            .sessionManagement()
            .sessionFixation().changeSessionId()
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .invalidSessionUrl("/")
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false)
            .and()
            .and() 
            ... ... ...
    ;
}

댓글