[ 정수원 스프링 시큐리티 #2 ] 스프링 시큐리티 주요 아키텍처 이해 (4)

김수호·2024년 3월 15일
0
post-thumbnail

지난 포스팅에 이어, 이번 포스팅에서는 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 는 익명 사용자용 필터이다. 이 필터는 현재 사용자의 인증 객체가 없는 경우에 동작한다. 그렇지 않은 경우 별다른 처리를 하지 않는다.

 

✔️ 참고


강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 정수원 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글