지난 포스팅에 이어, 이번 포스팅에서는 9) ~ 11)
의 내용을 정리한다.
👉 목차는 다음과 같다.
1) 위임 필터 및 필터 빈 초기화 - DelegatingProxyChain, FilterChainProxy
2) 필터 초기화와 다중 보안 설정
3) 인증 개념 이해 - Authentication
4) 인증 저장소 - SecurityContextHolder, SecurityContext
5) 인증 저장소 필터 - SecurityContextPersistenceFilter
6) 인증 흐름 이해 - Authentication Flow
7) 인증 관리자 : AuthenticationManager
8) 인증 처리자 : AuthenticationProvider
9) 인가 개념 및 필터 이해 : Authorization, FilterSecurityInterceptor
10) 인가 결정 심의자 : AccessDecisionManager, AccessDecisionVoter
11) 스프링 시큐리티 필터 및 아키텍처 정리
바로 하나씩 확인해보자.
9) 인가 개념 및 필터 이해 : Authorization, FilterSecurityInterceptor
이번 내용에서는 Authorization, FilterSecurityInterceptor 에 대해서 알아보자.
✔️ Authorization (인가)
: 당신에게 무엇이 허가되었는지 증명하는 것
- 참고)
- 스프링 시큐리티는 인증과 인가를 Authentication 과 Authorization 두 개의 영역으로 처리한다.
- 사용자가 어떤 자원에 접근을 요청할 때, 그 사용자가 인증된 사용자인지 아닌지를 먼저 판단한다. 이후 그 사용자가 가진 권한들이, 해당 자원에 설정된 권한에 충분한 자격을 갖추고 있는지를 판단한다.
👉 스프링 시큐리티가 지원하는 권한 계층에 대해서 알아보자.
(스프링 시큐리티는 세 가지 계층에 대해서 인가 처리를 할 수 있도록 지원한다.)
- 웹 계층
- URL 요청에 따른 메뉴 혹은 화면 단위의 레벨 보안
- 참고)
- 사용자가 /user 라는 경로로 자원에 접근을 요청하는 경우, 그 자원에 설정된 권한과 사용자가 가진 권한을 심사해서 최종적으로 해당 URL에 접근 가능한지 아닌지 여부를 판단해서 결정하는 계층.
- 서비스 계층
- 화면 단위가 아닌 메소드 같은 기능 단위의 레벨 보안
- 참고)
- 사용자가 user() 라는 메서드를 호출해서 진입하고자 하는 경우, 해당 메서드에 설정된 권한과 사용자가 가진 권한을 심사해서 인가 처리를 하는 계층.
- 도메인 계층(Access Control List, 접근제어목록)
- 객체 단위의 레벨 보안
- 참고)
- 사용자가 user 객체를 사용해서 작업을 처리하고자 하는 경우, 해당 도메인에 설정된 권한과 사용자가 가진 권한을 심사해서 인가 처리를 하는 계층.
- 참고) 이번 내용에서는 웹 계층과 서비스 계층에 대해서 다룬다.
✔️ FilterSecurityInterceptor
- 인가 처리를 담당하는 필터이다.
- 스프링 시큐리티가 제공하는 보안 필터 중, 가장 마지막에 위치한 필터로써, 인증된 사용자에 대하여 특정 요청의 승인/거부 여부를 최종적으로 결정한다.
- 만약, 인증 객체 없이 보호자원에 접근을 시도할 경우, AuthenticationExcpetion 을 발생시킨다.
- 만약, 인증 후 자원에 접근 가능한 권한이 존재하지 않을 경우, AccessDeniedException 을 발생시킨다.
- 권한 제어 방식 중 HTTP 자원의 보안을 처리하는 필터이다.
- 참고) URL 방식으로 자원에 접근할 경우에 해당 필터가 작동한다.
- 실질적인 권한 처리는 AccessDecisionManager 에게 맡긴다.
- 참고) AccessDecisionManager 는 처리시 내부적으로 AccessDecisionVoter 들을 활용한다.
👉 FilterSecurityInterceptor 의 인가 처리 흐름을 그림으로 확인해보자.
- 참고)
- 1) 사용자가 특정 자원에 접근을 요청한다.
- 2) FilterSecurityInterceptor 필터가 해당 요청을 받는다.
- 3) FilterSecurityInterceptor 필터는 해당 사용자의 인증 여부를 체크한다. ( SecurityContext 객체 안에 Authentication 인증 객체의 유무로 인증 여부를 체크 )
- (인증 객체가 null 인 경우) AuthenticationException 을 발생시키고, 더 이상의 인가 처리를 진행하지 않는다. 그리고 해당 예외는 ExceptionTranslationFilter 가 받아서 처리한다.
- (인증 객체가 null 이 아닌 경우) 사용자가 요청한 자원에 설정된 권한 정보를 SecurityMetadataSource 를 통해 조회한다.
- 4) 사용자가 요청한 자원에 접근하기 위해 필요한 권한 정보가 null 이라면(=해당 자원에 설정된 권한이 없는 경우), 따로 권한 심사를 하지 않는다. 따라서 자원 접근이 허용된다.
- 만약 권한 정보가 null이 아니라면(=해당 자원에 설정된 권한 정보가 있는 경우), 해당 권한 정보를 AccessDecisionManager(최종 심의 결정자) 에게 전달한다. (인가 처리 위임)
- 5) AccessDecisionManager 는 내부적으로 AccessDecisionVoter(심의자) 에게 심의 요청을 한다.
- 6) AccessDecisionVoter 는 현재 사용자가 자원에 접근할 자격이 있는지/없는지를 판단해서, 승인 또는 거부와 같은 결과 값을 AccessDecisionManager 에게 전달한다.
- 7) AccessDecisionManager 는 전달받은 결과 값을 가지고, 최종적으로 해당 자원에 접근을 승인할지 결정한다.
- 접근이 승인되면, 자원 접근이 허용된다.
- 접근이 거부되면, AccessDeniedException 을 발생시킨다. 그리고 해당 예외는 ExceptionTranslationFilter 가 받아서 처리한다.
10) 인가 결정 심의자 : AccessDecisionManager, AccessDecisionVoter
이번 내용에서는 AccessDecisionManager 와 AccessDecisionVoter 에 대해서 알아보자.
✔️ AccessDecisionManager
- FilterSecurityInterceptor 필터에서 전달받은 (인증 정보, 요청 정보, 권한 정보)를 이용해서 사용자의 자원접근을 허용할 것인지 거부할 것인지를 최종 결정하는 주체이다. ( 실질적으로 인가 처리를 총괄하는 역할을 한다. )
- 여러 개의 Voter 들을 가질 수 있으며, Voter 들로부터 (자원 접근에 대한) 접근 허용, 거부, 보류에 해당하는 각각의 값을 리턴받고 판단 및 결정한다.
- 최종 접근 거부 시 예외를 발생시킨다. (
AccessDeniedException
)
- 접근결정의 세 가지 유형
- 참고) AccessDecisionManager 는 인터페이스이다. 그리고 이 인터페이스를 구현한 세 가지 구현체가 있는데, 각 구현체들은 접근결정의 유형별로 나눠볼 수 있다.
- ① AffirmativeBased ( 기본 전략 )
- 여러개의 Voter 클래스 중 하나라도 접근 허가로 결론을 내면, 접근 허가로 판단한다.
- 참고)
- ② ConsensusBased
- 다수표(승인 및 거부)에 의해 최종 결정을 판단한다.
- 동수일 경우, 기본(default)은 접근 허가이나, allowIfEqualGrantedDeniedDecisions 을 false 로 설정할 경우 접근 거부로 결정된다.
- 참고)
- ③ UnanimousBased
- 모든 보터가 만장일치로 접근을 승인해야 하며 그렇지 않은 경우 접근을 거부한다.
- 참고)
✔️ AccessDecisionVoter
- 판단을 심사하는 것(위원)
- Voter 가 권한 부여 과정에서 판단하는 자료는 다음과 같다.
Authentication
- 인증 정보 (user)
FilterInvocation
- 요청 정보 (antMatcher("/user"))
ConfigAttributes
- 권한 정보 (hasRole("USER"))
- 결정 방식에는 세 가지가 있다.
ACCESS_GRANTED
: 접근 허용 (1)
ACCESS_DENIED
: 접근 거부 (-1)
ACCESS_ABSTAIN
: 접근 보류 or 기권 (0)
- Voter 가 해당 타입의 요청에 대해 결정을 내릴 수 없는 경우에 해당한다.
- 참고) Voter 들이 결정 방식에 따른 값을 AccessDecisionManager 에게 리턴하고, AccessDecisionManager 는 각각의 Voter 들이 전달하는 값들을 계산해서 최종 결정한다.
👉 AccessDecisionManager 와 AccessDecisionVoter 가 인가 처리를 하는 흐름을 살펴보자.
- 참고)
- FilterSecurityInterceptor 필터가 AccessDecisionManager 에게 인가 처리를 위임한다.
- AccessDecisionManager 는 자신이 가지고 있는 여러 AccessDecisionVoter 들에게 세 가지 정보(인증 정보, 요청 정보, 권한 정보)를 인자로 전달해서 권한 판단 심사를 맡긴다.
- AccessDecisionVoter 들은 해당 파라미터 값들을 근거로 해서 판단하고, (ACCESS_GRANTED or ACCESS_DENIED or ACCESS_ABSTAIN) 값을 반환한다.
- AccessDecisionManager 는 Voter 들로부터 반환된 값을 가지고 사용자의 최종적인 (접근 허용 / 접근 거부)를 결정한다.
- 접근 거부 시 AccessDeniedException 예외가 발생하고, 해당 예외는 ExceptionTranslationFilter 가 처리한다.
11) 스프링 시큐리티 필터 및 아키텍처 정리
지금까지 학습한 내용을 정리해보자.
- 참고)
- 참고) 위쪽 영역의 그림은 시큐리티가 초기화되는 과정이고, 아래쪽 영역의 그림은 각 필터들에 대한 처리 과정이다.
- 가정) 두 개의 SecurityConfig 설정 클래스를 만들었다. 그리고 그 안에서 httpSecurity 를 통해 여러개의 API들을 정의했고, 각 API들이 해당하는 요청을 받아서 처리하도록 구성했다.
스프링 시큐리티가 초기화될 때 진행되는 과정
- 각 설정 클래스 별로 정의된 API와 구성대로 필터들이 생성된다. 그리고 이 필터 목록들은 WebSecurity 클래스에게 전달된다.
- WebSecurity 는 FilterChainProxy 클래스의 빈 객체를 생성한다. 그리고 이때, 자신이 전달받은 필터 목록들을 생성자로 전달한다.
- 그러면 결과적으로 FilterChainProxy 는 WebSecurity 가 전달한 설정 클래스별 각 필터 목록들을 가지고 있게 된다.
- 서블릿 필터인 DelegatingFilterProxy 가 "springSecurityFilterChain" 라는 이름으로 생성된 빈을 찾아 사용자의 요청을 위임하도록 한다.
- 참고로 그 빈이 바로 FilterChainProxy 이다.
초기화 과정이 끝나고, 사용자의 각 요청에 따라 동작하는 주요 필터에 대해서 정리해보자.
1) 사용자가 인증을 시도하는 경우
- DelegatingFilterProxy 가 해당 요청을 받는다. 그리고 FilterChainProxy 에게 사용자의 요청을 위임한다.
- FilterChainProxy 는 자신이 가지고 있는 필터 목록을 순서대로 호출하면서 요청을 맡긴다.
- 먼저, SecurityContextPersistenceFilter 필터가 사용자 요청을 받아서 처리한다. 이 필터는 내부에 (SecurityContext 객체를 생성하고, 세션에 저장하고, 세션에 저장된 SecurityContext 를 조회하는 등의 역할을 하는) HttpSessionSecurityContextRepository 클래스를 통해 loadContext(..) 를 해서, 해당 사용자의 세션에 SecurityContext 객체가 있는지 체크한다. 현재 사용자는 인증에 시도하려는 사용자이기 때문에 세션에 SecurityContext 객체가 없다. 따라서, 새로운 SecurityContext 객체를 생성해서 SecurityContextHolder 에 담는다. (Create SecurityContext) 그리고 다음 필터로 이동한다.
- UsernamePasswordAuthenticationFilter 필터는 사용자가 입력한 id 와 password 를 추출해서 인증 객체를 생성한다. 그리고 AuthenticationManager(인증관리자) 에게 인증 처리를 맡긴다. AuthenticationManager 는 내부에서 관리하는 AuthenticationProvider 목록 중에서 사용자의 요청을 처리할 수 있는 적절한 객체를 찾고, 해당 객체에게 인증 처리를 위임한다. 그리고 위임받은 AuthenticationProvider 는 UserDetailsService 를 통해 사용자의 아이디와 패스워드 등을 검증한다. 만약 검증에 성공했다면, 최종적으로 인증에 성공한 정보를 담고있는 인증 객체가 생성되어 인증 필터로 전달되고, 인증 필터에서는 인증 성공에 따른 후속 처리를 진행한다. ( 인증 객체를 SecurityContext 객체에 저장하고, successHandler 를 호출하는 등 )
- 참고로 인증 성공에 따른 후속 처리를 진행하기 전에, 세션 관리 기능 필터인 SessionManagementFiler 를 거친다.
- SessionManagementFiler 필터에서는 크게 세 가지 정도의 처리 과정을 거친다.
- ConcurrentSession 을 통해 두 가지 전략(이전 사용자의 세션을 만료로 설정 or 현재 사용자 인증 시도 차단)으로 동시적 세션을 제어한다.
- 참고로 현재 해당 계정으로 처음 로그인을 시도하는 것이기 때문에, 여기서는 통과된다.
- SessionFixation 을 통해 세션 고정 보호를 처리한다.
- Register SessionInfo 를 통해 사용자의 세션 정보를 등록한다.
- SecurityContextPersistenceFilter 는 인증이 모두 완료되고 최종 응답이 되기 직전에 SecurityContext 를 세션에 저장한다. 그리고 SecurityContextHolder 에서 SecurityContext 를 삭제한다. (Clear SecurityContext)
2) 1) 에서 인증한 후, 어떤 자원에 접근하는 경우
- DelegatingFilterProxy 가 해당 요청을 받는다. 그리고 FilterChainProxy 에게 사용자의 요청을 위임한다.
- FilterChainProxy 는 자신이 가지고 있는 필터 목록을 순서대로 호출하면서 요청을 맡긴다.
- 먼저, SecurityContextPersistenceFilter 필터가 사용자 요청을 받아서 처리한다. 이 필터는 내부 HttpSessionSecurityContextRepository 클래스를 통해 loadContext(..) 를 해서, 해당 사용자의 세션에 SecurityContext 객체가 있는지 체크한다. 현재 사용자는 인증된 사용자로 세션에 SecurityContext 객체가 존재한다. 따라서 새로운 SecurityContext 객체를 생성하지 않고, 세션에 저장된 SecurityContext 를 꺼내서 SecurityContextHolder 에 담는다. 그리고 다음 필터로 이동한다.
- ExceptionTranslationFilter 필터는 FilterSecurityInterceptor 에서 발생하는 인증/인가 예외를 처리한다. 따라서 try - catch 로 감싸서 그 다음 필터를 호출한다.
- FilterSecurityInterceptor 필터는 인가 처리를 진행한다. 이 필터는 먼저 SecurityContext 안에 인증 객체가 존재하는지 체크한다. 인증 객체가 없으면 인가 처리를 할 수 없기 때문에, 즉시 인증 예외를 발생시킨다. 인증 객체가 있으면(인증된 사용자라면), AccessDecisionManager 에게 인가 처리를 맡긴다. AccessDecisionManager 는 내부적으로 AccessDecisionVoter 들을 가지고 최종적으로 해당 사용자의 자원에 대한 접근 승인 / 거부를 결정한다. (만약 사용자의 권한이 자원에 접근할 수 있는 권한이 아닌 경우 인가 예외를 발생시킨다. )
3) 1) 과 동일한 계정으로 인증을 시도하는 경우
- DelegatingFilterProxy 가 해당 요청을 받는다. 그리고 FilterChainProxy 에게 사용자의 요청을 위임한다.
- FilterChainProxy 는 자신이 가지고 있는 필터 목록을 순서대로 호출하면서 요청을 맡긴다.
- 먼저, SecurityContextPersistenceFilter 필터가 사용자 요청을 받아서 처리한다. 이 필터는 내부에 HttpSessionSecurityContextRepository 클래스를 통해 loadContext(..)를 해서, 해당 사용자의 세션에 SecurityContext 객체가 있는지 체크한다. 현재 사용자는 인증에 시도하려는 사용자이기 때문에 세션에 SecurityContext 객체가 없다. 따라서 새로운 SecurityContext 객체를 생성해서 SecurityContextHolder 에 담는다. (Create SecurityContext) 그리고 다음 필터로 이동한다.
- UsernamePasswordAuthenticationFilter 필터는 사용자가 입력한 id 와 password 를 추출해서 인증 객체를 생성한다. 그리고 AuthenticationManager(인증관리자) 에게 인증 처리를 맡긴다. AuthenticationManager 는 내부에서 관리하는 AuthenticationProvider 목록 중에서 사용자의 요청을 처리할 수 있는 적절한 객체를 찾고, 해당 객체에게 인증 처리를 위임한다. 그리고 위임받은 AuthenticationProvider 는 UserDetailsService 를 통해 사용자의 아이디와 패스워드 등을 검증한다. 만약 검증에 성공했다면, 최종적으로 인증에 성공한 정보를 담고있는 인증 객체가 생성되어 인증 필터로 전달되고, 인증 필터에서는 인증 성공에 따른 후속 처리를 진행한다. ( 인증 객체를 SecurityContext 객체에 저장하고, successHandler 를 호출하는 등 )
- 참고로 인증 성공에 따른 후속 처리를 진행하기 전에, 세션 관리 기능 필터인 SessionManagementFiler 를 거친다.
- SessionManagementFiler 필터에서는 크게 세 가지 정도의 처리 과정을 거친다.
- ConcurrentSession 을 통해 두 가지 전략(이전 사용자의 세션을 만료로 설정 or 현재 사용자 인증 시도 차단)으로 동시적 세션을 제어한다.
- 만약, 현재 시스템에서 동시적 세션 제어 전략으로 "현재 사용자의 인증 시도 차단"이 적용되었다면, SessionAuthenticationException 이 발생되고, 최종적인 인증에 성공하지 못한다.
- 만약, 현재 시스템에서 동시적 세션 제어 전략으로 "이전 사용자 세션 만료 설정"이 적용되었다면, 이전 사용자(
위 1)
)의 세션을 만료로 설정한다. ( 현재 해당 전략이라고 가정하자. )
- SessionFixation 을 통해 세션 고정 보호를 처리한다.
- Register SessionInfo 를 통해 사용자의 세션 정보를 등록한다.
- SecurityContextPersistenceFilter 는 인증이 모두 완료되고 최종 응답이 되기 직전에 SecurityContext 를 세션에 저장한다. 그리고 SecurityContextHolder 에서 SecurityContext 를 삭제한다. (Clear SecurityContext)
4) 1) 에서 인증한 사용자가 특정 자원에 접근하는 경우
- DelegatingFilterProxy 가 해당 요청을 받는다. 그리고 FilterChainProxy 에게 사용자의 요청을 위임한다.
- FilterChainProxy 는 자신이 가지고 있는 필터 목록을 순서대로 호출하면서 요청을 맡긴다.
- 먼저, SecurityContextPersistenceFilter 필터가 사용자 요청을 받아서 처리한다. 이 필터는 내부 HttpSessionSecurityContextRepository 클래스를 통해 loadContext(..) 를 해서, 해당 사용자의 세션에 SecurityContext 객체가 있는지 체크한다. 현재 사용자는 인증된 사용자로 세션에 SecurityContext 객체가 존재한다. 따라서 새로운 SecurityContext 객체를 생성하지 않고, 세션에 저장된 SecurityContext 를 꺼내서 SecurityContextHolder 에 담는다. 그리고 다음 필터로 이동한다.
- ConcurrentSessionFilter 가 동작한다. ConcurrentSessionFilter 필터는 매 요청마다 사용자의 세션 만료 여부를 확인(session.isExpired)한다. 그런데 위 3) 의 인증이 처리되는 과정에서, 1) 사용자의 세션이 만료로 설정되었다. 따라서 현재 사용자는 세션이 만료된 상태이므로, 즉시 로그아웃 처리되고, 오류 정보가 응답된다. 그리고 더 이상의 처리가 진행되지 않는다.
참고)
LogoutFilter
는 로그아웃을 처리하는 필터이다. 따라서 이 필터는 로그아웃 요청을 하지 않으면 특별하게 하는 역할은 없다.
UsernamePasswordAuthenticationFilter
는 인증 처리를 하는 필터이다. 따라서 이 필터는 인증 요청을 하지 않으면 특별하게 하는 역할은 없다.
ConcurrentSessionFilter
는 동시적인 세션에 관련된 처리를 하는 필터이다. 해당 필터는 최소한 동일한 계정으로 두명 이상이 접속을 시도하는 경우에 동작한다. 그렇지 않은 경우 별다른 처리를 하지 않는다.
RememberMeAuthenticationFilter
는 SecurityContext 객체 안에 Authentication 인증 객체가 null 이면서, 요청 헤더에 remember-me 쿠키 정보가 있는 경우 동작한다. 그렇지 않은 경우 별다른 처리를 하지 않는다.
AnonymousAuthenticationFilter
는 익명 사용자용 필터이다. 이 필터는 현재 사용자의 인증 객체가 없는 경우에 동작한다. 그렇지 않은 경우 별다른 처리를 하지 않는다.
✔️ 참고
강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 정수원 강사님께 있습니다.