본문 바로가기
Spring/Spring Security

[Spring Securiry] Security Filter

by yoon_seon 2023. 10. 28.
  1. SecurityContextPersistenceFilter
  2. BasicAuthenticationFilter
  3. UsernamePasswordAuthenticationFilter
  4. CsrFilter
  5. RememberMeAuthenticationFilter
  6. AnonymousAuthenticationFilter
  7. FilterSecurityInterceptor
  8. ExceptionTranslationFilter

스프링 시큐리티의 동작은 사실상 Filter로 동작한다고 봐도 무방하며 다양한 필터들이 존재하고 이 Filter들은 각자 다른 기능을 한다.

이런 Filter들은 제외할 수 있고 추가할 수 있으며 필터에 동작하는 순서를 정해줘서 원하는 대로 동작하게 할 수 있다.

 

FilterChainProxy에서 doFilterInternal에 디버깅해 보면 실제로 많은 필터들이 적용돼 있는 것을 확인할 수 있다.

FilterOrderRegistration을 확인해 보면 각각의 필터에 100씩 order를 채번 하고 있다.

100씩 증가하는 order 사이이 개발자가 커스텀한 필터를 넣어 순서를 조작할 수 있는 것이다.

 


📌 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter는 보통 두 번째로 실행되는 필터이다.(첫 번째로 실행되는 필터는 Async 요청에 대해서도 SecurityContext를 처리할 수 있도록 해주는 WebAsyncManagerIntegrationFilter 임)

 

SecurityContextPersistenceFilter는 SecurityContext를 찾아와서 SecurityContextHolder에 넣어주는 역할을 한다.

만약 SecurityContext가 없다면 새로 생성한다

loadContext()를 통해 SecurityContext를 불러오고 SecurityContextHolder에 넣은 후 그 후 chain.doFilter를 통해 다음 필터를 호출하는 것을 확인할 수 있다.

loadContext는 HttpSession에서 SecurityContext를 불러온다.

 

 

HttpSession에서 SecurityContext를 불러오는 방법

  1. 로그인을 하면 JSESSIONID라는 쿠키를 받는다.
  2. 유저 A의 브라우저는 쿠키를 저장한다.
  3. 로그인 후 다음 서버 요청에 JSESSIONID를 포함하여 요청한다.
  4. 서버에서는 쿠키로 세션을 찾았기 때문에 찾은 세션으로 SecurityContext를 불러오고 로그인이 유지된다.

 

실제로 쿠키가 저장되는 것을 확인

크롬 확장 프로그램인 EditThisCookie를 통해서 확인했다.

로그인 전 후 쿠키확인

로그인 전에는 쿠키가 없지만, 로그인 후 JSESSIONID가 서버에 의해 생성된 것을 확인할 수 있다.

EditThisCookie를 통해 쿠키를 삭제하면 다음 요청 시 로그아웃된다. 그 이유는 SecurityContext를 찾을 수 없기 때문이다.

 


📌 BasicAuthenticationFilter

보통 로그인 후 접근가능한 페이지를 로그인하지 않고 접근하면 로그인 페이지로 이동한다.

 

로그인이 필요한 페이지를 로그인 없이 curl -u 명령어로 HTTP 요청해 보면

로그인 페이지의 결과가 응답된다.

(curl 명령어는 url은 URL을 사용하여 데이터를 전송하는 명령줄 도구 및 라이브러리입니다. -u 옵션은 HTTP 요청을 할 때 기본 인증에 사용할 사용자 이름과 비밀번호를 지정하는 데 사용됩니다.)

SpringSecurityConfig에서 http.httpBasic()을 통해서 BasicAuthenticationFilter를 활성화하고 다시 요청해 보면

 

로그인 없이도 로그인이 필요한 페이지의 응답이 오는 것을 확인할 수 있다.

 

로그인 과정이 없이도 <username>/<password>라는 로그인 데이터를 base64로 인코딩해서 요청에 포함하여 보내면 BasicAuthenticationFilter는 이것을 인증한다.

그래서 세션이 필요 없고 올 때마다 인증이 이루어지는 것이다.(stateless 하다.)

이런 방식은 요청할 때마다 아이디와 비밀번호가 반복해서 노출되기 때문에 보안에 취약하며 어쩔 수 없이 BasicAuthenticationFilter를 사용해야 하는 상황에서는 반드시 https를 사용하도록 권장된다.

 


📌 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter는 Form 데이터로 username, password 기반의 인증을 담당하고 있는 필터다.

 

로그인 요청이 들어오면 아래와 같은 순서로 인증이 이루어진다.

UsernamePasswordAuthenticationFilter  username, password 인증 필터
ProviderManager(AuthenticationManager) 인증 정보 제공 관리자
AbstractUserDetailsAuthenticationProvider 인증 정보 제공. 계정의 상태나 패스워드 일치 여부 등을 파악
DaoAuthenticationProvider 유저 정보 제공
UserDetailsService 유저 정보를 제공하는 Service

실제로 로그인 폼에서 로그인 요청을 보내면

왼쪽에서 오른쪽으로 브레이크 포인트를 찍어놓은 순서대로 디버깅이 걸리는 것을 확인할 수 있음.

 

UserDetailsService는  UserDetailsService를 재정의해서 구현하면 개발자가 의도한 방식으로 로그인정보가 맞는지 검증한다.

 


📌 CsrFilter

CsrFilter는 Csrf 토큰을 사용하여 위조된 페이지의 악의적인 공격을 방어하는 역할을 하는 Filter이다.

가짜 페이지를 만들어서 정상적인 시스템에 악의적인 요청을 하는 것을 CsrfAttack라고 한다.

정상적인 페이지는 Csrf 토큰이 있을 것이고 위조된 페이지는 Csrf 토큰이 변조되거나 없을 것이다.

 

CsrFilter는 자동으로 활성화되어 있는 Filter지만 명시적으로 활성화하기 위해서는 http.csrf() 코드를 추가하고 비활성화하기 위해서는 http.csrf().disable() 코드를 추가한다.

 


📌 RememberMeAuthenticationFilter

RememberMeAuthenticationFilter는 세션시간을 조정할 수 있도록 하는 역할을 한다.

Session의 기본 만료 시간은 기본 설정이 30분이지만 RememberMeAuthenticationFilter는 기본 설정이 2주이다.

JSESSIONID는 브라우저를 종료하면 사라지지만 RememberMe는 유지된다.

RememberMeAuthenticationFilter는 장시간 남아있는 RememberMe 쿠키를 이용해서 세션을 다시 유지시켜 주는 역할을 한다.

(서버를 재시작한다면 클라이언트에는 RememberMe 쿠키가 남아있지만 서버에서는 RememberMe 토큰을 잃어버리기 때문에 로그아웃 된다.)

 

RememberMeAuthenticationFilter는 자동으로 활성화되어 있는 Filter지만 명시적으로 활성화하기 위해서는 http.csrf() 코드를 추가하고 비활성화하기 위해서는 http.RememberMeAuthenticationFilter().disable() 코드를 추가한다.

그리고 화면에서 로그인기억하기의 cheakbox의 name을 'remember-me'로 설정해줘야 한다.(기본값 예약어이며 설정 쪽에서 수정 가능)

remember-me의 값이 true이면 서버에서는 응답으로 rememberMe 토큰을 주게 된다.

 


📌  AnonymousAuthenticationFilter

인증이 안된 유저가 요청하면 Annonymous 유저로 만들어 Authentication에 넣어주는 역할을 하는 필터이다.

인증되지 않아도 null을 넣는 게 아니라 기본 Authentication을 만들어주는 개념.

다른 Filter에서는 Annonymous 유저인지 정상 인증 유저인지 분기처리 가능하다.

 

로그인 정보 없이 SecurityContext를 디버깅하면 확인할 수 있음.

 

이전 필터와 동일하게 SpringSecurityConfig에서 설정 가능하며 

http.anonymous().principal(); 을 통해서 익명 유저에  principal 부여 가능함.

 


📌  FilterSecurityInterceptor

이전 필터에서 넘겨준 authentication의 내용을 기반으로 최종 인가 판단을 내리는 역할을 하며 대부분의 경우 필터 중 뒤쪽에 위치한다.

먼저, 인증(authentication)을 가져오고 만약 인증에 문제가 있다면 AuthenticationException을 발생하고 문제가 없다면 해당 인증을 인가로 판단한다. 이때 인가가 거절된다면 AccessDeniedException이 발생하고 승인된다면 정상적으로 필터가 종료된다.

 

AbstractSecurityInterceptor.class

authenticateIfRequired() 메서드를 통해서 인증정보를 가져오는 것을 확인할 수 있다.

 

attemptAuthoriztion() 메서드에서는 decide() 메서드를 통해 인증 정보를 가져온다. 

인증 정보가 없다면 AccessDeniedException을 발생시킨다.

 

AffirmativeBased에서 구현한 decide() 메서드를 확인해 보면

AccessDecisionVoter에 의해서 인가를 승인해야 할지를 판단한다.

DENIED(거절되는 건)이 한 건이라도 있다면 AccessDeniedException을 발생시킨다.

 


📌  ExceptionTranslationFilter

앞서 본 FilterSecurityInterceptor에서 발생할 수 있는 두 가지 Exception을 핸들링하여 후처리 하는 필터다.

  1. AuthenticationException : 인증에 실패할 때 발생
  2. AccessDeniedException : 인가에 실패할 때 발생

ExceptionTranslationFilter의 handleSpringSecurityException는 Exception 종류에 따른 로직을 분산한다.

 

 

기본설정으로 AuthenticationException 발생 또는 Anonymous의 AccessDeniedException가 발생하면 로그인 페이지로 이동한다.

로그인 유저에서 AccessDeniedException이 발생한다면 권한이 없는 것 이기 때문에 403 화이트라벨 페이지로 이동시킨다.

 

handleSpringSecurityException에서 AuthenticationException 발생 시 호출하는 handleAuthenticationException을 보면

sendStartAuthentication()에 의해 로그인 페이지로 이동시키는 것을 확인할 수 있다.

 

다음으로

handleSpringSecurityException에서 AccessDeniedException 발생 시 호출하는 handleAccessDeniedException을 보면

익명 유저일 경우 sendStartAuthentication()에 의해 로그인 페이지로 이동시키고 익명 유저가 아니라면 

403 페이지로 이동시키는 것을 확인할 수 있다.

 

이렇게 Exception이 일어났을 때 이에 대한 후처리를 할 수 있는 필터이다.

댓글