스프링 시큐리티 아키텍처와 인증 및 권한 부여

스프링 시큐리티를 처음 접하면 몇 줄의 설정만으로 강력한 보안 기능을 적용할 수 있다는 점에 놀라게 된다. 하지만 애플리케이션의 요구사항이 복잡해질수록 단순한 기본 설정만으로는 부족함을 느끼게 된다. 실제 구현 단계에서는 단순 설정에서 제공하는 내용보다 훨씬 더 복잡한 내부 아키텍처가 작동하고 있으며, 각 요소가 어떻게 상호작용하는지 이해하지 못하면 커스텀 설정을 적용할 때 많은 어려움을 겪을 수 있다.

이 글에서는 스프링 시큐리티 서적의 내용을 바탕으로 보호된 웹 요청의 전체 흐름과 인증(Authentication), 권한 부여(Authorization)의 핵심 메커니즘을 상세히 살펴보고자 한다.


1. 웹 요청 처리의 핵심: 서블릿 필터와 위임

스프링 시큐리티는 주로 서블릿 필터를 활용해 웹 애플리케이션 요청 컨텍스트를 둘러싼 기능 레이어를 제공한다. 서블릿 필터들은 리소스에 도달하기 전 전처리를 수행하거나 요청 전체를 리다이렉트하는 용도로 사용된다.

물리적인 서블릿 필터 체인은 사용자의 요청을 처리하는 데 사용되는 메서드 체인 내의 연결 고리를 나타낸다. 스프링 시큐리티는 필터 체인 개념을 사용하여 별도의 추상 체인인 VirtualFilterChain을 구현하고, URL 패턴에 따라 이 체인이 동적으로 구성되게 해 준다.

이때 중요한 역할을 하는 것이 DelegatingFilterProxy이다. web.xml 파일에서 필터로 선언되는 이 클래스는 스프링의 ApplicationContext에서 설정된 빈을 찾아 요청을 위임한다. 즉, 서블릿 컨테이너와 스프링의 IoC 컨테이너 사이를 연결하는 다리 역할을 수행하여, 스프링 시큐리티가 제공하는 필터 체인들이 실행될 수 있도록 돕는다.


2. 사용자 인증의 흐름: Authentication과 UserDetails

사용자가 로그인 폼에 정보를 입력하면 시스템은 일련의 검증 절차를 거친다. 이 과정에서 가장 핵심적인 인터페이스는 AuthenticationManager와 AuthenticationProvider이다.

AuthenticationManager는 인증 요청을 받아 적절한 AuthenticationProvider에게 실제 검증 작업을 위임한다. AuthenticationProvider는 사용자의 자격 증명(크리덴셜)을 검증하고, 인증에 성공할 경우 사용자의 권한 정보가 포함된 Authentication 객체를 반환한다.

이 과정에서 혼동하기 쉬운 두 가지 인터페이스가 있는데, 바로 Authentication과 UserDetails이다.

  • Authentication: 인증 요청과 관련된 컨텍스트, 주체(Principal), 자격 증명 등에 대한 정보를 저장하는 보안 전용 객체이다.

  • UserDetails: 이름, 이메일, 전화번호 등 주체의 프로필 정보를 저장하고 업무상의 요구조건을 지원하기 위해 확장해 사용하는 것이 일반적이다. 즉, Authentication 내부의 Principal이 UserDetails 인스턴스를 포함할 수 있는 구조이다.


3. 기본 로그인 페이지의 동작 방식

스프링 시큐리티는 기본적으로 로그인 페이지를 제공한다. URL에서 spring_security_login 부분은 DefaultLoginPageGeneratingFilter 클래스에 저장된 기본 로그인 페이지를 나타낸다. 물론 실제 서비스에서는 이 페이지를 그대로 사용하기보다 애플리케이션에 적합한 디자인으로 수정하여 사용하게 된다.

로그인 폼이 제출되면 요청은 스프링 시큐리티가 감시하는 특수 URL인 spring_security_check로 전송된다. UsernamePasswordAuthenticationFilter는 이 URL로 들어오는 요청을 가로채어 폼 기반 로그인을 처리한다. 이때 폼의 input 태그 이름은 기본적으로 j_username과 j_password로 설정되어 있는데, 이는 자바 EE 서블릿 표준을 따른 것이다.


4. 권한 부여와 접근 결정: AccessDecisionManager와 투표

인증이 완료되었다고 해서 모든 리소스에 접근할 수 있는 것은 아니다. 특정 요청을 받아들일지 거부할지를 결정하는 일은 FilterSecurityInterceptor가 담당하며, 실제 권한 부여 결정은 AccessDecisionManager에게 위임된다.

AccessDecisionManager는 decide 메서드를 통해 접근 승인 여부를 결정한다. 이 과정은 투표(vote)라는 개념을 통해 이루어지는데, AccessDecisionManager는 여러 개의 AccessDecisionVoter를 거느리고 이들의 투표 결과를 취합하여 최종 결정을 내린다.

각 보터(Voter)는 다음 세 가지 중 하나의 의견을 낸다.

  1. ACCESS_GRANTED: 접근 권한을 줄 것을 권장함

  2. ACCESS_DENIED: 접근 권한을 거부할 것을 권장함

  3. ACCESS_ABSTAIN: 결정을 보류함 (판단할 수 없는 경우)

이러한 투표 결과를 바탕으로 최종 결정을 내리는 방식에는 여러 가지가 있다. 예를 들어 AffirmativeBased는 보터 중 하나라도 승인하면 접근을 허용하고, ConsensusBased는 다수결을 따르며, UnanimousBased는 모든 보터가 만장일치로 승인해야 접근을 허용한다.


5. SpEL 표현식을 통한 유연한 보안 설정

스프링 시큐리티 3부터는 SpEL(Spring Expression Language)을 사용하여 더욱 복잡하고 정교한 접근 제어 규칙을 정의할 수 있게 되었다. 기존의 단순한 역할(ROLE) 기반 체크를 넘어, 표현식 언어를 통해 특정 조건이나 메서드 호출 결과를 보안 설정에 반영할 수 있다.

설정 파일에서 use-expressions=”true” 속성을 추가하면 SpEL을 사용할 수 있다. 이를 통해 hasRole(‘ROLE_USER’)와 같이 직관적인 표현이 가능하며, isAnonymous()나 isAuthenticated() 같은 의사 속성을 통해 접근 조건을 명확하게 기술할 수 있다. 이러한 처리는 내부적으로 WebExpressionVoter를 통해 이루어지며, 이는 SpEL 표현식을 해석하여 투표에 참여하는 방식으로 작동한다.


스프링 시큐리티는 겉보기에는 간단한 설정으로 동작하는 것 같지만, 그 내부에는 필터 체인, 인증 매니저, 접근 결정 관리자, 보터 등 수많은 객체가 유기적으로 협력하는 정교한 아키텍처가 숨어 있다. 이러한 내부 구조와 각 컴포넌트의 역할을 이해하는 것은 단순히 보안 기능을 적용하는 것을 넘어, 애플리케이션의 특수한 요구사항에 맞춰 보안 전략을 커스터마이징하고 문제를 해결하는 데 필수적인 기반이 된다.

댓글 남기기