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

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

지난 포스팅에 이어, 이번 포스팅에서는 3) ~ 5) 의 내용을 정리한다.

👉 목차는 다음과 같다.

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) 스프링 시큐리티 필터 및 아키텍처 정리

바로 하나씩 확인해보자.


3) 인증 개념 이해 - Authentication

Authentication(인증)
: 당신이 누구인지 증명하는 것

  • 스프링 시큐리티에서는 Authentication 을 사용자의 인증 정보를 저장하는 토큰 개념으로 사용한다.
    • 인증 시, id 와 password 를 담고, 인증 검증을 위해 전달되어 사용된다.
    • 인증 후, 최종 인증 결과 (user 객체, 권한정보)를 담고, SecurityContext 에 저장되어 전역적으로 참조가 가능하다.
      • Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  • 구조
    • 1) principal : 사용자 아이디 or User 객체를 저장
    • 2) credentials : 사용자 비밀번호
    • 3) authorities : 인증된 사용자의 권한 목록
    • 4) details : 인증 부가 정보
    • 5) authenticated : 인증 여부
  • 참고) 스프링 시큐리티는 기본적으로 인증에 사용할 수 있는 Authentication 구현체가 제공된다.
    • 물론 우리가 직접 Authentication 인터페이스를 구현해서 만들어 사용할 수도 있다.

 

그러면 Authentication 인터페이스가 (인증 시 / 인증 후)에 어떻게 활용되는지 흐름을 살펴보자.

  • 참고)
    • 1) 사용자가 폼 로그인 인증을 시도한다.
    • 2) UsernamePasswordAuthenticationFilter 인증 필터가 사용자 요청 정보를 받아, username 과 password 를 추출한 다음, Authentication 객체를 생성하여 인증 검증을 위한 처리에 사용한다.
      • principal: 사용자 아이디를 담는다.
      • credentials: 패스워드를 담는다.
      • authorities: null로 설정한다.
      • authenticated: false로 설정된다.
    • 3) UsernamePasswordAuthenticationFilter 는 AuthenticationManager 에 인증 처리를 요청한다.
      • 인증 실패시 예외가 발생하고, 인증 성공시 AuthenticationManager 는 Authentication 타입의 객체를 생성한다.
        • principal: 최종적으로 인증에 성공한 User 객체의 결과를 담는다.
        • credentials: 패스워드를 담는데, 보안상 비워두기도 한다.
        • authorities: 권한 목록을 담는다.
        • authenticated: true로 설정한다.
    • 4) 생성된 인증 결과 객체는 SecurityContextHolder 내부 SecurityContext 객체에 저장된다. 그렇게 되면 해당 인증 객체를 전역적으로 사용할 수 있게 된다.
      • 참고로 ThreadLocal 에 대한 내용은 다음 내용에서 학습한다.

 

👉 위 내용을 코드를 통해서 확인해보자.

  • SecurityConfig 보안 정책 작성
  • 서버를 기동해서 로그인을 시도해보자.
    • 사용자가 인증을 요청함에 따라 해당 요청이 UsernamePasswordAuthenticationFilter 인증 필터로 들어온 것을 확인할 수 있다. 그리고 인증 필터에서는 사용자가 입력한 username 과 password 를 추출해서, Authentication 인터페이스를 구현한 UsernamePasswordAuthenticationToken 인증 객체에 해당 정보를 담는다.
    • 이후 해당 인증 객체를 AuthenticationManager(인증 관리자)에 전달해서 인증 처리를 맡긴다.
  • AuthenticationManager 의 구현체인 ProviderManager 를 디버깅 해보자.
    • ProviderManager 는 자신이 가지고 있는 AuthenticationProvider 타입의 객체들 중 현재 사용자의 인증을 처리할 수 있는 객체에 처리를 위임한다. ( 이후 최종 성공한 인증 객체 결과를 전달받으면 UsernamePasswordAuthenticationFilter 에게 전달한다. )
    • 참고) AbstractUserDetailsAuthenticationProvider 를 디버깅 해보자.
      • 사용자 인증 처리에 성공한 경우, UsernamePasswordAuthenticationToken 객체를 생성해서 최종 성공한 인증 결과를 저장하는 것을 확인할 수 있다.
  • 참고) UsernamePasswordAuthenticationToken 을 잠시 확인해보자.
    • UsernamePasswordAuthenticationToken 클래스를 보면, 두개의 생성자가 있다.
    • 1) (principal, credentials) 파라미터를 전달받는 첫 번째 생성자
      • 인증 시, 사용자가 입력한 id 와 password 정보를 담을 때 사용한다.
    • 2) (principal, credentials, authorities) 파라미터를 전달받는 두 번째 생성자
      • 인증 후, 최종 인증 결과를 담을 때 사용한다.
  • 인증에 성공한 이후 최종적으로 인증 필터에서는, 인증 결과가 담긴 인증 객체를 SecurityContext 안에 담는 것을 확인할 수 있다.
    • 인증 필터에서 저장한 최종 인증 결과 객체는 SecurityContextHolder.getContext().getAuthentication() 구문을 통해, 위치나 장소에 관계없이 어디에서나 전역적으로 참조가 가능하다.

4) 인증 저장소 - SecurityContextHolder, SecurityContext

✔️ SecurityContext

  • Authentication 객체가 저장되는 보관소로, 필요 시 언제든지 Authentication 객체를 꺼내어 쓸 수 있도록 제공되는 클래스이다.
  • ThreadLocal 에 저장되어 아무 곳에서나 참조가 가능하도록 설계함
    • 참고) ThreadLocal 은 Thread 마다 고유하게 할당된 저장소를 의미한다. 따라서 Thread 간 공유가 되지 않고 각 Thread 에게만 할당되어 있는 저장소이다.
    • 참고) SecurityContext 는 요청 스레드별로 ThreadLocal 에 저장되는 개념이다.
  • 인증이 완료되면 HttpSession 에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능하다.

✔️ SecurityContextHolder

  • SecurityContext 를 감싸고 있는 클래스이다.
  • SecurityContext 객체 저장 방식
    • MODE_THREADLOCAL (기본값) : 스레드당 SecurityContext 객체를 할당한다.
    • MODE_INHERITABLETHREADLOCAL : 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext 를 유지한다.
    • MODE_GLOBAL : 응용 프로그램에서 단 하나의 SecurityContext를 저장한다.
    • 참고) SecurityContextHolder
      • SecurityContextHolder 는 3가지 모드로 SecurityContext 객체 저장 방식을 지원한다.
      • 참고로 기본 모드는 MODE_THREADLOCAL 이고, 설정 클래스에서 다른 전략으로 변경할 수 있다.
    • 참고) ThreadLocalSecurityContextHolderStrategy 클래스를 확인해보자.
      • 내부를 보면 ThreadLocal 객체가 선언되어 있고, 해당 ThreadLocal 에 SecurityContext 객체를 저장하거나, 조회할 수 있도록 구현되어 있다.
  • SecurityContextHolder.clearContext() : SecurityContext 기존 정보 초기화

 

위 내용을 그림으로 이해해보자.

  • 참고)
    • 1) 사용자가 로그인을 시도한다.
    • 2) 서버에서 로그인 요청을 받는다. 그러면 서버에서는 사용자의 요청을 처리할 하나의 쓰레드를 할당한다.
      • 참고) 쓰레드 마다 ThreadLocal 이라는 쓰레드 전용 저장소가 할당된다.
    • 3) 그러면 해당 쓰레드가 인증 처리를 진행하고, 인증 필터가 동작한다.
      • 인증 필터에서는 이전 내용에서 살펴본 것 처럼, Authentication 객체를 생성해서, 여기에 사용자의 로그인 정보를 저장하고, 인증 처리를 시도한다.
    • 4) 만약 인증에 실패하게 되면, SecurityContextHolder 안에 있는 SecurityContext 객체를 초기화 한다.
      • SecurityContextHolder.clearContext()
    • 5) 만약 인증에 성공하게 되면, 인증 필터는 SecurityContextHolder 안에 SecurityContext 객체에, 최종적으로 인증에 성공한 결과 정보를 담고 있는 인증 객체를 저장한다.
      • 구조) SecurityContextHolder 가 ThreadlLocal 객체를 가지고 있고, ThreadlLocal 이 SecurityContext 를 담고있다.
    • 6) 최종적으로 SecurityContext 객체는 "SPRING_SECURITY_CONTEXT" 라는 이름으로 HttpSession 에도 저장된다.

 

👉 위 내용을 코드를 통해서 확인해보자.

  • SecurityConfig 보안 설정 추가
  • SecurityController 수정: 로그인 후 인증 객체를 참조할 수 있도록 코드를 추가해보자.
    • 인증 객체를 조회하는 방법에는 여러가지 방법이 있다. (참고로 조회된 인증객체(authentication, authentication1) 는 모두 동일한 객체이다.)
    • 참고) HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"
  • 서버를 기동해서 디버깅 해보자.
    • 스프링 시큐리티가 초기화 되는 과정속에서, SecurityContextHolder 의 기본 전략을 설정하고 있다. ( 기본 전략은 MODE_THREADLOCAL 방식이다. )
  • 로그인 페이지에서 인증을 시도해보자.
    • 인증 필터에서 인증에 성공한 인증 객체를 SecurityContext 객체에 저장하는 것을 확인할 수 있다.
  • 디버깅 모드에서 다음 스텝으로 이동하면, 이제 인증에 성공하고 루트 경로로 이동한다.
    • 그러면 인증 성공시 저장했던 인증 객체(UsernamePasswordAuthenticationToken@8307)를 확인할 수 있다. ( 참고로 어떤 방식으로 인증 객체를 조회하던 다 동일한 객체인 것을 확인할 수 있다. )
  • SecurityController 추가: 이번엔 메인 쓰레드와 자식 쓰레드간 SecurityContext 객체가 공유되지 않는 것을 확인해보자.
    • 자식 스레드에서 메인 스레드 ThreadLocal 에 저장된 인증 객체를 불러오는지 확인해보자.
  • 서버 기동 후 인증하여 /thread 경로로 접근해보자.
    • 인증 객체가 null 인 것을 확인할 수 있다. ( 기본 전략 모드인 MODE_THREADLOCAL 로는 자식 스레드와 부모 스레드 간 SecurityContext 객체를 공유할 수 없다. )
  • SecurityConfig 수정: 설정 클래스에서 모드를 변경해보자.
    • MODE_INHERITABLETHREADLOCAL 모드로 변경했다. 그러면 스프링 시큐리티가 초기화 될 때 해당 방식으로 모드 전략을 적용한다.
  • 서버를 기동하여, 루트 경로로 접근해보자.
    • 인증 객체를 확인할 수 있다. (UsernamePasswordAuthenticationToken@6674)
  • /thread 경로에 접근해보자.
    • 이제 부모 스레드의 ThreadLocal 에 저장된 인증 객체를 자식 스레드에서도 참조할 수 있는 것을 확인할 수 있다.

5) 인증 저장소 필터 - SecurityContextPersistenceFilter

✔️ SecurityContextPersistenceFilter
: SecurityContext 객체를 생성, 저장, 조회하는 역할을 하는 필터이다.

  • 익명 사용자의 요청 시
    • SecurityContextPersistenceFilter 는 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장한다. ( 저장 후 다음 필터로 이동한다. )
    • 이후 AnonymousAuthenticationFilter 에서 AnonymousAuthenticationToken 객체를 SecurityContext 에 저장한다.
      • 참고) AnonymousAuthenticationToken: 익명사용자용 인증 객체
  • 익명 사용자의 인증 요청 시
    • SecurityContextPersistenceFilter 는 새로운 SecurityContext 객체를 생성하여, SecurityContextHolder 에 저장한다. ( 저장 후 다음 필터로 이동한다. )
    • 이후 UsernamePasswordAuthenticationFilter 에서 인증에 성공하면, SecurityContext 에 UsernamePasswordAuthenticationToken 객체를 SecurityContext 에 저장한다.
    • 인증이 최종 완료되면 Session 에 SecurityContext 를 저장한다.
  • 인증된 사용자의 요청 시
    • SecurityContextPersistenceFilter 는 새로운 SecurityContext 를 생성하지 않고, Session 에서 SecurityContext 객체를 꺼내서 SecurityContextHolder 에 저장한다.
    • 참고) 따라서 인증 이후에는 별도의 인증 처리 과정을 거치지 않더라도, SecurityContext 안에 Authentication 객체가 존재하기 때문에 계속적으로 사용자 인증을 유지할 수 있게 된다.
  • (공통) 최종 응답 시
    • SecurityContextPersistenceFilter 는 SecurityContextHolder.clearContext() 를 해서 SecurityContext 를 삭제한다.
  • (정리) SecurityContextPersistenceFilter 는 어떤 요청이든 간에 SecurityContext 객체를 SecurityContextHolder 에 저장하는 역할을 한다.
    • 여기서, 익명 사용자의 요청 또는 익명 사용자의 인증 요청의 경우에는, 새로운 SecurityContext 객체를 생성해서 저장하고,
    • 인증된 사용자의 요청 시에는 세션에서 SecurityContext 객체를 꺼내서 저장한다.
  • (참고) SecurityContextPersistenceFilter 는 스프링 시큐리티 보안 필터 중 두 번째에 위치하며, 이후 필터들이 SecurityContextHolder 에서 SecurityContext 객체를 꺼내서 참조한다.
  • (참고) SecurityContextPersistenceFilter 는 내부에 HttpSessionSecurityContextRepository 를 가지고 있는데, 이 클래스가 실제로 SecurityContext 객체를 생성하고 조회하는 역할을 담당한다.
    • SecurityContextPersistenceFilter 를 보면, 내부에 SecurityContextRepository 를 가지고 있는 것을 확인할 수 있다. 그리고 HttpSessionSecurityContextRepository 는 SecurityContextRepository 의 구현체 클래스이다. 해당 클래스에서 SecurityContext 객체를 생성하고 조회하는 역할을 한다.

 

그림으로 흐름을 이해해보자.

  • 참고)
  • 1) 사용자가 요청을 한다.
  • 2) SecurityContextPersistenceFilter 는 사용자의 요청을 받아, 내부에 HttpSessionSecurityContextRepository 클래스를 통해 해당 사용자가 인증 전인지 아닌지를 체크한다. (세션에 SecurityContext 가 있는지 체크)
    • 참고) SecurityContextPersistenceFilter 는 사용자의 어떠한 요청이든지 동작한다.
  • 3) 인증 전의 요청(익명 사용자의 요청 or 익명 사용자의 인증 요청)이라면, 새로운 SecurityContext 를 생성해서 SecurityContextHolder 에 저장한다. ( 참고로 이때는 Authentication 인증 객체는 null 이다. ) 그리고 그 다음 필터로 이동한다.
    • 인증 필터(UsernamePasswordAuthenticationFilter or AnonymousAuthenticationFilter)가 인증을 처리한다. 그리고 인증에 성공했다면, 인증 필터는 SecurityContext 객체 안에 인증에 성공한 Authentication 인증 결과 객체를 저장한다. 그리고 그 다음 필터로 계속 이동한다.
    • 이후 최종적으로 클라이언트에게 응답하는 시점에, SecurityContextPersistenceFilter 는 SecurityContext 를 세션에 저장한다. 그리고 SecurityContext 를 SecurityContextHolder 에서 제거시킨다.
      • 참고) AnonymousAuthenticationFilter 가 동작하는 익명 사용자의 요청인 경우는 세션에 저장하지 않는다.
    • 최종적으로 클라이언트에게 응답한다.
  • 4) 인증 후의 요청이라면, 세션에서 SecurityContext 객체를 꺼내서, SecurityContextHolder 에 저장한다. ( 참고로 이때는 SecurityContext 안에 Authentication 인증 객체가 저장되어 있다. ) 그리고 그 다음 필터들로 이동한다.
    • 참고로 SecurityContext 안에 인증 객체가 있기 때문에 이 요청은 별도의 인증 처리 과정을 거치지 않는다. 인증 객체가 있으므로 스프링 시큐리티가 이미 인증 받은 상태라고 판단한다.
    • 이후 최종적으로 클라이언트에게 응답하는 시점에, SecurityContext 를 SecurityContextHolder 에서 제거시킨다.
    • 최종적으로 클라이언트에게 응답한다.

 

두 가지 경우를 요약하면 다음과 같다.

  • 참고)
    • 인증을 받기 전 : SecurityContextPersistenceFilter 는 모든 요청에 대해 새로운 SecurityContext 를 생성한다.
      • 생성되는 구조를 보면, SecurityContextHolder 안에 ThreadLocal 이 있고, 그 안에 SecurityContext 가 있다. 그리고 이때는 SecurityContext 안에 Authentication 객체는 없다. 이후 인증 필터를 거치면서 SecurityContext 객체 안에 인증 객체가 저장된다.
    • 인증을 받은 후 : SecurityContextPersistenceFilter 는 세션에서 SecurityContext 객체를 꺼내와서 SecurityContextHolder 에 저장한다.
    • 참고) 각 객체들의 관계를 자연스럽게 익혀두자.
      • SecurityContextHolder > ThreadLocal > SecurityContext > Authentication > User, Authrorities...

 

👉 (익명 사용자의 요청 시, 익명 사용자의 인증 요청 시, 인증된 사용자의 요청 시) 3가지 경우를 나눠서 SecurityContextPersistenceFilter 가 처리하는 과정을 확인해보자.

  • 1) 익명 사용자 요청의 경우
    • 서버 기동 후, 루트 경로에 접근해보자.
      • SecurityContextPersistenceFilter 가 요청을 받았다. 그리고 repo(HttpSessionSecurityContextRepository) 를 통해 loadContext(holder)를 하고 있다.
    • HttpSessionSecurityContextRepository 를 확인해보자.
      • loadContext 에서는 세션에서 SecurityContext 객체를 조회한다. 세션에 SecurityContext 가 존재하는 경우는 인증된 사용자에 해당하고, 없는 경우는 인증되지 않은 사용자에 해당한다. 만약 세션에 SecurityContext 가 없다면 SecurityContext 를 생성한다.
      • 그리고 SecurityContextHolder 내부 ThreadLocal 에 SecurityContext 를 저장하고, 다음 필터로 이동하여 요청을 처리한다.
        • 현재 익명 사용자이기 때문에 인증 필터로는 AnonymousAuthenticationFilter 가 동작한다. 해당 필터에서는 익명 사용자용 인증 객체를 만들어서 SecurityContext 에 저장한다.
      • 처리가 끝나고 나면, finally 구문에서 SecurityContextHolder.clearContext() 로 SecurityContext 객체를 삭제한다.
  • 2) 인증 요청을 하는 경우
    • 서버 기동 후 로그인을 시도해보자.
    • SecurityContextPersistenceFilter 이 동작한다. 그리고 사용자는 인증을 받지 않았기 때문에 세션에 SecurityContext 가 존재하지 않는다. 따라서 SecurityContext 객체를 생성한다.
    • 그리고 SecurityContextHolder 내부 ThreadLocal 에 SecurityContext 를 저장하고, 다음 필터로 이동하여 요청을 처리한다.
      • 여기서는 인증을 시도하기 때문에, 인증 필터로 UsernamePasswordAuthenticationFilter 가 동작한다. 해당 필터에서는 인증에 성공하게 되면, 인증에 성공한 인증 객체를 SecurityContext 에 저장한다.
    • 처리가 끝나고 나면, SecurityContextHolder.clearContext() 를 통해 SecurityContext 객체를 삭제한다. 그리고 세션에 SecurityContext 를 저장(saveContext)한다.
  • 3) 인증 후 요청 시
    • 인증 후 새로고침 해보자.
    • SecurityContextPersistenceFilter 이 동작한다. 그리고 이제는 세션에 SecurityContext 가 존재한다. 따라서 SecurityContext 객체를 새로 생성하지 않고, 세션에서 가져온다.
    • 가져온 SecurityContext 객체를 SecurityContextHolder 내부 ThreadLocal 에 저장하고, 다음 필터로 이동하여 요청을 처리한다.
      • 참고로 SecurityContext 안에 인증 객체가 있기 때문에 이 요청은 인증 처리 과정을 거치지 않고도, 스프링 시큐리티가 이미 인증 받은 상태라고 판단한다.
      • 처리가 끝나고 나면, SecurityContextHolder.clearContext() 로 SecurityContext 객체를 삭제한다.

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

profile
현실에서 한 발자국

0개의 댓글