반복영역 건너뛰기
주메뉴 바로가기
본문 바로가기
제품/서비스
EMS Solution
Features
클라우드 관리
서버관리
데이터베이스 관리
네트워크 관리
트래픽 관리
설비 IoT 관리
무선 AP 관리
교환기 관리
운영자동화
실시간 관리
백업 관리
스토리지 관리
예방 점검
APM Solution
애플리케이션 관리
URL 관리
브라우저 관리
ITSM Solution
서비스데스크
IT 서비스 관리
Big Data Solution
SIEM
AI 인공지능
Dashboard
대시보드
Consulting Service
컨설팅 서비스
고객
레퍼런스
고객FAQ
문의하기
가격
자료실
카탈로그
회사소개
비전·미션
연혁
2016~현재
2000~2015
인증서·수상
투자정보
재무정보
전자공고
IR자료
새소식
공고
보도자료
오시는 길
채용
피플
컬처
공고
FAQ
블로그
열기
메인 페이지로 이동
블로그
최신이야기
블로그
최신이야기
사람이야기
회사이야기
기술이야기
다양한이야기
최신이야기
검색
기술이야기
Spring Security로 로그인부터 사용자 전환까지 구현하기
기술이야기
Spring Security로 로그인부터 사용자 전환까지 구현하기
ID/PW 로그인으로 시작한 인증 기능도 제품의 접점이 늘어나면 다양한 요구사항을 만나게 됩니다. 외부 시스템에는 API Key를 이용한 호출이 필요하고, 개발 과정에서는 사용자 ID만으로 권한별 화면을 확인해야 할 때가 있습니다. 운영 지원을 위해 관리자가 하위 사용자의 실행 환경으로 전환하는 기능도 필요합니다. 이러한 요구사항을 구현하려면 인증 정보를 검증하는 방식뿐 아니라 요청을 구분하고 인증 상태를 유지하는 구조도 함께 이해해야 합니다. 이 글에서는 먼저 Spring Security의 공식 구조를 바탕으로 로그인 처리 흐름을 살펴보겠습니다. 이어서 프로젝트에서 세 가지 인증 방식을 분리한 방법을 자세히 알아보고, 마지막으로 원래 로그인한 사용자를 보존하면서 실행 사용자를 전환하는 구현을 살펴보겠습니다. 1장: Servlet 필터 체인부터 인증 관리자·제공자·토큰·세션까지 2장: ID/PW, API Key, 개발용 ID 인증의 커스텀 구현 3장: 상위 사용자의 하위 사용자 전환과 원래 계정 복귀 ※ 프로젝트 코드는 설명에 필요한 부분만 발췌했으며, 암호화·복호화·비밀번호 검증의 세부 구현과 실제 인증값은 주석으로 생략했습니다. 1. 공식 문서로 이해하는 로그인 처리 구조 로그인을 구현할 때는 요청을 받는 코드와 사용자를 검증하는 코드의 역할을 먼저 구분해야 합니다. Spring Security는 이를 필터, 인증 관리자, 인증 제공자, 인증 결과 객체로 나누어 처리합니다. 요청이 이 구성 요소들을 거쳐 인증되고 저장되는 흐름을 이해하면, 이후에 살펴볼 여러 로그인 방식과 사용자 전환도 같은 구조 안에서 파악할 수 있습니다. ※설명 기준: 프로젝트는 Spring Boot 3.0.4를 사용하며, Spring Security 버전은 6.0.2입니다. 이 장은 Spring Security 6.x의 Servlet 구조를 설명하며, 그림과 설명은 공식 6.5 레퍼런스를 사용했습니다. 아래 설정은 학습용 예시로, 다음 장의 프로젝트 설정과는 구분됩니다. 1.1. Servlet 컨테이너의 FilterChain에 들어가는 보안 필터 먼저 요청이 들어오는 지점부터 살펴보겠습니다. Spring Security의 보안 필터가 연결되는 대상은 Apache Tomcat 같은 Servlet 컨테이너가 실행하는 Servlet Filter 체계입니다. 간혹 이를 “Apache 필터”라고 표현하기도 하지만, Apache HTTP Server의 모듈이나 필터 체계와는 구분해야 합니다. 그림 1. 컨테이너의 필터들과 Servlet. 요청은 여러 필터를 거쳐 Spring MVC의 DispatcherServlet에 도착합니다. 각 필터는 요청을 다음 단계로 넘기거나 직접 응답하고 처리를 종료할 수 있습니다. Spring Security도 이 구조를 이용해 인증이 필요한 요청을 컨트롤러에 도착하기 전에 차단할 수 있습니다. 이 과정에서 컨테이너의 필터와 Spring Bean을 연결하는 클래스가 DelegatingFilterProxy입니다. 이 프록시는 Spring에서 관리하는 보안 필터 Bean으로 처리를 위임합니다. 일반적으로 위임 대상인 springSecurityFilterChain Bean의 실제 구현은 FilterChainProxy이며, 이름에 Chain이 포함되어 있지만 Servlet 관점에서는 하나의 Filter로 동작합니다. 그림 2. Servlet 컨테이너와 Spring ApplicationContext를 연결하는 프록시. 이 구조를 요청의 이동 순서에 따라 살펴보겠습니다. 컨테이너의 필터 체인에서 보안 처리 지점에 도착한 요청은 DelegatingFilterProxy를 통해 FilterChainProxy로 전달됩니다. FilterChainProxy는 요청에 맞는 SecurityFilterChain을 선택하고, 그 안의 보안 필터들을 순서대로 실행합니다. 보안 처리를 통과한 요청은 컨테이너의 필터 체인으로 돌아가 Servlet까지 진행합니다. 공식 문서의 Architecture에서도 이러한 계층을 각각의 그림으로 설명합니다. 1.2. 필터 매니저와 패턴 매처: 어떤 보안 체인을 적용할까 그렇다면 여러 보안 체인이 등록되어 있을 때는 어떤 체인을 선택할까요? 앞서 살펴본 FilterChainProxy가 이 선택을 담당하며, 이 글에서 말하는 “필터 매니저” 역할에 해당합니다. 우선순위가 높은 체인부터 RequestMatcher로 요청을 검사하고, 처음 일치한 체인 하나만 실행합니다. 일치하는 체인을 모두 이어서 실행하는 방식은 아닙니다. 그림 3. 요청에 처음 일치한 SecurityFilterChain 선택. 이 선택 구조를 설정하려면 요청 매처의 역할을 구분해야 합니다. 먼저 securityMatcher와 requestMatchers가 어떤 단계에서 사용되는지 알아보겠습니다. 두 설정 모두 요청을 비교하지만, 체인을 선택하는 단계와 선택된 체인 안에서 접근 권한을 확인하는 단계로 역할이 나뉩니다. 로그인 필터에도 별도의 요청 매처가 있으며, 각 역할은 다음과 같습니다. 예를 들어 /api/** 전용 체인이 먼저 선택되면 뒤쪽 웹 체인의 formLogin 설정은 해당 요청에 적용되지 않습니다. 같은 이유로 /secured/**만 담당하는 체인에 기본 /login 처리를 설정하면 로그인 요청이 그 체인에 도달하지 못합니다. 또한 어느 체인에도 일치하지 않는 요청은 Spring Security의 보호를 받지 않습니다. 마지막에 전체 요청을 담당하는 체인을 두는 이유도 이러한 선택 구조 때문입니다. 1.3. 인증 관리자, 인증 제공자, 토큰의 역할 요청에 적용할 체인이 정해지면 그 안에서 실제 인증이 진행됩니다. 이때 흔히 “authenticator”라고 부르는 역할은 하나의 클래스에 모여 있지 않습니다. 인증 필터가 입력을 해석하고, AuthenticationManager가 인증 요청을 받으며, AuthenticationProvider가 해당 인증 방식을 검증합니다. AuthenticationManager의 대표 구현인 ProviderManager는 등록된 인증 제공자에게 검증을 위임합니다. 각 제공자는 supports를 통해 자신이 처리할 수 있는 Authentication 타입을 알립니다. 이는 해당 토큰의 처리 가능 여부를 나타내며, 로그인 성공을 의미하지는 않습니다. 이처럼 역할을 분리하면 요청에서 자격 증명을 추출하는 방식과 사용자 검증 정책을 각각 확장할 수 있습니다. 필터와 인증 제공자 사이에서 전달되는 입력과 결과는 Authentication 객체로 표현합니다. ID/PW 인증에서는 UsernamePasswordAuthenticationToken을 사용할 수 있습니다. 검증 전에는 ID와 비밀번호를 담고, 검증 후에는 사용자 객체와 부여된 권한을 가진 인증 결과를 받습니다. 여기서 Token은 Spring Security 내부의 인증 객체를 뜻하며, 이 객체를 사용한다고 해서 JWT가 발급되거나 브라우저에 문자열 토큰이 반환되는 것은 아닙니다. 이제 인증 요청과 결과가 코드에서 어떻게 연결되는지 살펴보겠습니다. 공식 인증 아키텍처 예제에서 인증 요청과 결과를 받는 부분만 축약했으며, HTTP 응답과 세션 저장 처리는 생략했습니다. 위 코드의 request는 아직 검증되지 않은 인증 요청입니다. authenticate 호출을 통해 제공자의 검증을 마친 뒤에야 인증에 성공한 결과를 사용할 수 있습니다. 저장된 사용자 정보로 인증하는 기본 구현인 DaoAuthenticationProvider도 이 흐름을 따릅니다. UserDetailsService로 사용자를 조회하고, PasswordEncoder로 입력한 비밀번호와 저장된 값을 검증합니다. 사용자 조회와 비밀번호 검증은 별개의 책임이므로, 사용자 정보를 조회했다는 사실만으로 로그인이 성공하지는 않습니다. 검증을 마치면 사용자와 권한이 담긴 Authentication을 반환합니다. 비밀번호 보관·검증에 사용하는 구체적인 알고리즘은 이 글의 범위에서 생략합니다. 1.4. 표준 ID/PW 로그인 한 번 따라가기 이제 앞에서 살펴본 구성 요소들이 표준 로그인에서 어떻게 연결되는지 예제를 통해 알아보겠습니다. 공식 Form Login 예제의 핵심 설정은 formLogin입니다. 아래 코드는 해당 예제에 접근 규칙과 반환문을 추가한 학습용 예시이며, 사용자 저장소와 인증 제공자는 별도로 준비되어 있다고 가정합니다. 이 설정의 기본 폼은 POST /login으로 username, password 파라미터를 보냅니다. CSRF 보호가 켜져 있다면 폼에 CSRF 토큰도 포함해야 합니다. JSON 요청 본문으로 로그인 정보를 전달하는 API는 기본 폼 파라미터 처리와 별도로 입력 처리를 구성해야 합니다. 로그인 요청이 들어온 이후에는 앞서 설명한 인증 구성 요소들이 다음 순서로 동작합니다. 1. UsernamePasswordAuthenticationFilter가 로그인 요청에서 ID/PW를 추출합니다. 2. 미인증 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에 전달합니다. 3. 인증 제공자가 사용자와 자격 증명을 검증하고 인증 결과를 반환합니다. 4. 인증에 성공하면 세션 인증 전략을 실행하고 인증 결과를 SecurityContext에 설정합니다. 5. 성공 핸들러가 후속 응답을 생성합니다. 인증에 실패하면 실패 핸들러가 응답합니다. 이처럼 표준 로그인에서는 자격 증명 검증을 마친 뒤에도 세션 처리와 응답 생성이 이어집니다. 프로젝트의 커스텀 로그인 역시 이 후속 흐름까지 연결해야 합니다. 특히 로그인 상태를 다음 요청에서도 사용하려면 인증 결과를 어디에 저장하는지 확인해야 합니다. 1.5. 이번 요청의 인증을 다음 요청까지 유지하기 현재 사용자 정보는 SecurityContextHolder가 보관하는 SecurityContext의 Authentication으로 표현됩니다. 기본적인 Servlet 환경에서는 이 Holder가 요청 처리 스레드와 연결됩니다. 따라서 현재 요청에서 사용할 인증 정보를 설정하는 작업과, 다음 요청에서 다시 읽을 수 있도록 저장하는 작업은 구분해야 합니다. 요청 간 인증 상태를 연결하는 역할은 SecurityContextRepository가 담당합니다. 세션 기반 로그인에서는 브라우저가 다음 요청에 세션 쿠키를 보내면 서버가 저장된 컨텍스트를 복원합니다. Spring Security 6의 기본 SecurityContextHolderFilter는 컨텍스트를 읽는 역할을 담당하므로, 커스텀 인증 코드에서는 저장소에 명시적으로 저장해야 합니다. 기본 폼 로그인 필터가 처리하던 인증 흐름을 직접 구현한다면 이 저장 과정도 함께 연결해야 합니다. 인증 정보를 저장하는 것과 별개로, 새 인증에 어떤 세션 정책을 적용할지도 결정해야 합니다. 이를 담당하는 구성 요소가 SessionAuthenticationStrategy입니다. 동시 로그인 제한을 예로 들어 세션 정책을 어떻게 설정하는지 살펴보겠습니다. 아래는 공식 예제의 핵심 부분이며, 프로젝트가 같은 정책을 적용했다는 의미는 아닙니다. 이 설정은 기존 로그인을 유지하면서 두 번째 로그인을 거절합니다. maxSessionsPreventsLogin(true)를 생략하면 기본적으로 기존 세션을 만료시키는 방향으로 동작합니다. 세션 종료 이벤트를 추적하려면 HttpSessionEventPublisher도 연결해야 합니다. 또한 직접 만든 인증 필터에는 동시 세션 제어를 명시적으로 구성해야 합니다. 여기까지의 흐름은 요청에 맞는 체인 선택, 인증 객체 생성, 제공자의 검증, 인증 결과 저장으로 정리할 수 있습니다. 이 순서에 맞춰 코드를 읽으면 여러 인증 방식이 공통으로 사용하는 부분과 서로 다르게 구현한 부분을 구분하기 쉽습니다. 다음 장에서는 이 구조를 ID/PW, API Key, 개발용 ID 인증에 각각 적용한 방식을 살펴보겠습니다. 2. 세 가지 인증 요구사항을 Spring Security 확장 지점에 연결하기 프로젝트에는 인증이 필요한 세 가지 접점이 있습니다. 사용자는 화면에서 ID와 비밀번호로 로그인하고, 외부 연동 프로그램은 API Key로 API를 호출합니다. 개발자는 화면을 구현하면서 사용자 ID만으로 특정 사용자의 권한을 확인해야 합니다. 이러한 요구사항을 처리하기 위해 요청을 선택하는 SecurityFilterChain, 자격 증명을 해석하는 필터, 실제 검증을 수행하는 AuthenticationProvider의 역할을 나누었습니다. 여기서 ID는 DB의 숫자형 사용자 식별자가 아니라 사용자가 입력하는 loginName을 뜻합니다. ID/PW 인증과 개발용 인증에는 UsernamePasswordAuthenticationToken을 사용하고, API Key 인증에는 별도의 ZngApiKeyAuthenticationToken을 사용합니다. 인증 요청에 사용하는 Token은 다르지만, 인증 결과의 principal은 MyUserDetails로 통일했습니다. 따라서 이후 서비스는 동일한 사용자 모델과 메서드 권한 검사 체계를 사용할 수 있습니다. MyUserDetails는 Spring Security의 UserDetails 인터페이스를 구현한 프로젝트의 사용자 클래스입니다. Kotlin에서는 클래스 선언에 : UserDetails를 붙여 구현합니다. UserDetails는 사용자명, 권한 목록, 계정 잠금·활성 상태 등 인증에 필요한 정보를 제공하는 인터페이스입니다. 프로젝트에서는 여기에 사용자 ID와 계층, 소속 관리자, 접근 가능한 대상 그룹, 원래 로그인한 사용자 정보 등의 업무 속성을 추가했습니다. 인증 성공 후 MyUserDetails를 Authentication.principal에 넣으면 Spring Security의 권한 검사와 업무 코드의 현재 사용자 조회가 같은 객체를 기준으로 동작합니다. 다만 사용자 객체를 제공하는 것과 자격 증명을 검증하는 것은 별개의 역할입니다. UserDetails 구현만으로 비밀번호나 계정 상태가 자동 검증되지는 않으며, 이 프로젝트에서는 커스텀 Provider가 필요한 검사를 직접 호출합니다. getPassword()도 일반적인 저장 비밀번호 제공 경로로 사용하지 않습니다. 뒤에서 살펴볼 ID/PW Provider와 개발용 Provider 역시 목적에 따라 검증 범위가 다릅니다. 이러한 공통 사용자 모델을 바탕으로 각 인증 방식의 요청 구분과 결과 저장 범위를 다음과 같이 구성했습니다. 이제 요청 분류부터 시작해 각 인증 방식의 구현을 차례로 살펴보겠습니다. 이 장의 코드는 현재 저장소 구현에서 발췌했으며, 패키지와 import는 생략했습니다. 자격 증명 처리 부분은 주석으로 대체했고, 암호화·복호화·해시 알고리즘과 파라미터, 실제 인증값은 포함하지 않았습니다. 2.1 먼저 인증 방식이 충돌하지 않도록 요청을 분류합니다 세 가지 인증 방식을 함께 사용하려면 먼저 각 요청이 어느 체인으로 들어갈지 정해야 합니다. 앞 장에서 설명한 것처럼 여러 체인이 선언되어 있어도 처음 일치한 체인 하나만 요청을 처리합니다. 따라서 @Order와 securityMatcher의 조합은 단순한 실행 순서 설정을 넘어 인증 정책의 일부가 됩니다. 프로젝트에서는 세 가지 인증 진입점보다 앞에 인증 방식의 개수를 검사하는 체인을 두었습니다. 여기서 확인하는 항목은 로그인 요청 여부, 세션에 저장된 SecurityContext, Authorization 헤더입니다. 세션 쿠키의 존재만으로 판단하지 않고, HttpSessionSecurityContextRepository.containsContext(request)로 실제 저장된 컨텍스트를 확인합니다. 위 함수의 결과는 Order 40 체인의 matcher에서 사용합니다. 값이 1이 아닌 요청은 이 체인에서 선택해 401로 응답합니다. 예를 들어 세션으로 로그인한 요청에 API Key 헤더를 추가하거나, POST /api/login에 Authorization 헤더를 함께 보내면 두 방식이 겹칩니다. 이 경우 뒤쪽의 API Key 체인이나 세션 인증 체인으로 넘어가지 않습니다. 다만 이 단계에서는 Authorization 헤더의 형식이 아니라 존재 여부만 확인합니다. 이 검사의 적용 범위는 앞서 선택되는 체인까지 함께 보아야 이해할 수 있습니다. OPTIONS(Order 10), 정적 리소스(Order 20), 공개키 조회 GET /api/user/pk(Order 30), 비정상 로그아웃 처리(Order 35), 특정 보고서 생성 요청(Order 37)은 Order 40보다 먼저 선택될 수 있습니다. 이후에는 별도의 보고서용 인증(Order 50), 개발용 Basic(Order 60), API Key(Order 70), 세션 인증(Order 80)이 이어집니다. 따라서 모든 URL이 동일한 검사를 거치는 것은 아니며, @Order와 matcher를 기준으로 흐름을 확인해야 합니다. 각 인증 체인으로 분기할 때 사용하는 헤더 matcher는 대소문자를 무시하는 startsWith(authScheme, true)로 구현되어 있습니다. 이는 scheme에 따라 요청을 구분하는 역할이며, 엄격한 헤더 문법 검증까지 수행하는 구현은 아닙니다. 이렇게 요청 경로를 나눈 뒤, 각 필터와 Provider에서 인증 방식에 맞는 처리를 진행합니다. 2.2 ID/PW 로그인: 입력 형식과 사용자 정책을 분리합니다 먼저 일반 로그인은 POST /api/login 요청의 JSON body를 읽습니다. 이를 담당하는 ZngUsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속하며, 생성자에서 ProviderManager(ZngUsernamePasswordAuthenticationProvider(userService))를 전달합니다. 필터는 입력과 HTTP 요청을 처리하고 Provider는 사용자를 검증하므로, 역할 분담은 표준 로그인과 같습니다. 표준 폼 로그인과 달라지는 부분은 입력 형식입니다. 폼 파라미터 대신 JSON 본문을 읽어야 하므로 커스텀 필터를 사용했습니다. 필터는 입력을 준비한 뒤 다음과 같이 미인증 Token을 생성합니다. 이 Token은 검증할 요청을 전달하는 객체이며, 아직 인증 성공 상태는 아닙니다. 이때 details에 추가한 HTTP 요청 정보는 이후 IP 정책 검사에 사용합니다. Token을 전달받은 Provider는 서비스에 ID/PW 검증을 위임하고, 필요한 비밀번호 저장 형식 이전 처리를 수행한 뒤 사용자 상태를 추가로 확인합니다. 서비스에서는 ID 오류와 비밀번호 오류를 구별해 알리지 않고 동일한 인증 예외로 처리합니다. 자격 증명 검증 이후에는 어떤 항목을 추가로 확인하는지 코드로 살펴보겠습니다. 검증에 성공하면 principal은 문자열 ID에서 MyUserDetails로 바뀌고, authorities에는 사용자 권한이 포함되며, credentials는 null로 반환됩니다. 이 Provider의 supports는 전달된 Token이 UsernamePasswordAuthenticationToken 계열인지 확인합니다. ProviderManager는 이 지원 여부를 기준으로 검증을 담당할 Provider를 선택합니다. 인증 결과를 받은 다음에는 세션 정책을 적용하고 로그인 상태를 저장해야 합니다. 이를 위해 로그인 필터에 다음과 같은 세션 전략과 성공 핸들러를 지정했습니다. 먼저 세션 전략의 첫 번째 구성 요소는 설정값으로 최대 동시 세션 수를 확인합니다. 한도를 넘으면 신규 로그인을 예외로 거절하거나 기존 세션을 expireNow()로 만료 표시합니다. 두 번째 전략은 새 세션을 SessionRegistry에 등록합니다. 만료 표시된 세션으로 후속 요청이 들어오면 세션 체인에 배치한 ConcurrentSessionFilter가 처리합니다. 즉, 세션에 만료 여부를 기록하는 시점과 해당 세션의 다음 요청을 차단하는 시점은 구분됩니다. 다만 현재 동시 세션 전략에는 sortedBy의 반환값을 사용하지 않는 코드가 있어, 주석의 의도만으로 가장 오래 사용하지 않은 세션이 반드시 선택된다고 설명하기는 어렵습니다. 또한 위 Composite에 직접 포함된 전략은 동시 세션 검사와 등록 두 가지이므로, 이 설정만으로 세션 ID 변경 전략까지 적용된다고 단정할 수는 없습니다. 세션 전략을 통과하면 성공 핸들러가 SecurityContext를 생성하고 HttpSessionSecurityContextRepository.saveContext로 명시적으로 저장합니다. 다음 요청에서 세션에 저장된 인증 정보를 사용할 수 있는 이유입니다. 성공 응답은 리다이렉트 대신 MyResult JSON으로 반환하며, 로그인 실패 핸들러는 401을 반환합니다. 로그아웃 요청인 POST /api/logout에는 Spring Security의 로그아웃 처리와 감사 기록을 연결하고, 성공 JSON을 응답하도록 구성했습니다. 인증 결과를 통계와 감사 처리에 반영하는 흐름은 이벤트로 연결했습니다. ProviderManager에 AuthenticationEventPublisher를 설정하고, SecurityConfig의 이벤트 리스너에서 인증 성공 시 사용자 로그인 시도 횟수와 마지막 로그인 시각을 갱신합니다. 사용자 관련 실패와 API Key 자체의 실패는 서로 다른 이벤트로 매핑해 실패 횟수를 갱신할 대상도 구분합니다. 다만 이 이벤트는 Provider의 인증 결과를 알리는 것이므로, 이후 동시 세션 전략까지 성공했다는 의미는 아닙니다. 2.3 API key 인증: 사용자 인증에 호출 범위를 더합니다 ID/PW 로그인이 브라우저의 세션을 유지하기 위한 흐름이라면, 외부 연동에서는 Authorization: ZNG <API_KEY> 형식의 헤더로 인증 정보를 전달합니다. 실제 키는 예시에 포함하지 않았습니다. Order 70 체인이 이 헤더를 가진 요청을 선택하면, BasicAuthenticationFilter 위치에 배치한 ZngApiKeyAuthenticationFilter가 처리를 담당합니다. 이 필터는 OncePerRequestFilter를 상속합니다. 헤더에서 키를 읽고 형식을 확인한 뒤, 식별자와 검증용 비밀값을 미인증 ZngApiKeyAuthenticationToken에 담습니다. 여기에 요청 details를 추가해 API Key 전용 ProviderManager로 전달합니다. 인증 결과를 받은 뒤에는 컨텍스트를 설정하고 저장합니다. 이 과정이 ID/PW 로그인과 어떻게 다른지 다음 코드와 함께 살펴보겠습니다. 앞서 본 로그인 처리와 코드 형태는 비슷하지만, 여기서 사용하는 저장소는 RequestAttributeSecurityContextRepository입니다. API Key 필터는 HttpSession에 인증 결과를 직접 저장하지 않고 요청 속성 저장소를 사용하므로, 의도한 인증 유지 범위는 현재 요청입니다. 다만 설정에 SessionCreationPolicy.STATELESS가 명시된 것은 아니어서, 이 코드만으로 애플리케이션 전체에서 어떤 이유로도 세션이 생성되지 않는다고 볼 수는 없습니다. 이 인증 결과를 만드는 ZngApiKeyAuthenticationProvider는 먼저 식별자로 키를 조회하고, 비밀값 일치 여부와 활성·만료·잠금 상태를 확인합니다. 이어서 키에 연결된 사용자도 조회합니다. 키가 유효하더라도 사용자가 휴면·잠금·자격 증명 만료·비활성 상태이거나 IP 조건을 만족하지 못하면 인증하지 않습니다. 검증에 성공하면 ZngApiKeyAuthenticationToken.authenticated(user, null, user.authorities, apiKey)를 반환합니다. 전용 Token에 apiKey를 남기는 이유는 인증 이후에도 키별 호출 범위를 확인해야 하기 때문입니다. 다음 인가 코드에서 이 정보를 사용합니다. 위 코드는 인증과 인가의 역할을 구분해 보여줍니다. Provider가 “어떤 사용자와 키인가”를 검증했다면, ZngApiKeyAuthorizationManager는 “이 키로 이 요청을 호출해도 되는가”를 판단합니다. 요청은 키에 허용된 API 경로와 HTTP 메서드 matcher에 일치해야 하며, 사용자 전환·사용자 관리·API Key 관리 등 별도로 선언한 금지 matcher에도 해당하지 않아야 합니다. 이 제한은 선언된 matcher 범위에 적용되며, 모든 관리 기능을 자동으로 차단한다는 의미는 아닙니다. 컨트롤러와 서비스의 메서드 권한 검사도 별도로 작동합니다. 실패 처리도 API Key 인증 흐름에 맞추어 구성했습니다. 필터에서 AuthenticationException이 발생하면 감사 기록을 남기고 컨텍스트를 비운 뒤 403 진입점으로 응답합니다. 키 형식이 맞지 않으면 필터 자체에서 예외를 던지는 대신 다음 필터로 넘기지만, 인증된 API Key Token이 없으면 이후 인가를 통과할 수 없습니다. API Key로 들어오는 로그아웃 요청은 세션 로그아웃과 구분되므로, 앞쪽의 비정상 로그아웃 처리 체인에서 별도로 처리합니다. 2.4 개발용 ID 인증: 기본 HTTP Basic 필터에 Provider만 교체합니다 마지막으로 개발 중 사용자별 권한과 화면 동작을 확인하기 위한 인증 방식을 살펴보겠습니다. 이때는 지정한 사용자의 컨텍스트로 API를 호출할 수 있도록, 기본 HTTP Basic 처리에 ID만 조회하는 Provider를 연결했습니다. 요청을 해석하는 기본 필터는 사용하면서 사용자 검증 부분을 별도로 구성한 것입니다. 여기서 “ID만으로 로그인”한다는 것은 HTTP body에 숫자 ID 하나를 보내는 별도 로그인 API를 뜻하지 않습니다. 요청은 Basic 헤더 형식을 따르며, 기본 필터가 이를 UsernamePasswordAuthenticationToken으로 변환합니다. 이후 커스텀 Provider가 authentication.name, 즉 loginName으로 사용자를 조회합니다. 프런트엔드의 개발 로그인 설정도 입력받은 사용자명을 Basic 형식으로 준비해 후속 요청의 Authorization 헤더에 넣습니다. 실제 인증값을 만드는 표현식은 본문에서 생략했습니다. 위 Provider는 비밀번호를 비교하지 않으며, 일반 로그인 Provider에서 수행하는 사용자 상태·IP 검사도 호출하지 않습니다. 검증을 단순화한 개발 편의 기능이므로 활성화 범위를 함께 확인해야 합니다. 현재 조건은 **`prod` 프로필이 활성화되지 않은 환경(@Profile("!prod"))**입니다. 반면 API Key 체인에는 이 프로필 조건이 없습니다. 개발용 Basic 체인이 활성화된 환경에서도 ZNG 헤더 요청은 API Key 체인으로 들어가며, 두 인증 방식은 헤더에 따라 구분됩니다. 앞서 설명한 Order 40의 인증 방식 개수 검사도 개발용 요청에 적용됩니다. 따라서 세션 컨텍스트를 유지한 채 Basic 헤더를 추가하면 인증 방식이 충돌한 요청으로 처리됩니다. 지금까지 세 가지 인증 요구사항을 필터·Provider·Token·Context·세션 전략의 역할에 맞추어 연결한 방식을 살펴봤습니다. 다음 장의 사용자 전환은 이렇게 인증된 사용자가 다른 사용자의 컨텍스트로 이동하는 기능입니다. 다음 장에서는 새 자격 증명을 검증하는 로그인과 비교하면서, 전환 권한을 판단하고 세션 상태를 유지하는 방법을 알아보겠습니다. 3. 상위 사용자에서 하위 사용자로: 로그인 상태를 유지하며 실행 주체 바꾸기 운영 지원 과정에서는 관리자가 하위 사용자의 화면과 데이터 접근 범위를 확인해야 할 때가 있습니다. 매번 상대 사용자의 비밀번호를 받아 다시 로그인하는 방식으로는 이러한 요구를 처리하기 어렵습니다. 프로젝트에서는 이미 인증된 상위 사용자의 권한으로 전환 가능 여부를 판단하고, 이후 요청에서 사용할 Authentication을 대상 사용자의 것으로 교체했습니다. 이때는 두 사용자를 구분해야 합니다. 하나는 현재 기능을 실행하는 사용자이고, 다른 하나는 처음 로그인하여 전환 권한의 근거가 된 사용자입니다. 현재 사용자는 화면과 업무 권한의 기준이며, 최초 사용자는 다른 사용자로 이동하거나 원래 계정으로 돌아갈 수 있는 권한의 근거입니다. 이 구분을 유지하기 위해 먼저 원래 로그인한 사용자 정보를 보관합니다. 3.1. 원래 로그인한 사용자는 한 번만 보관합니다 MyUserDetails에는 원래 로그인한 사용자를 보관하는 orgLoginUser 필드가 있습니다. 값이 null이면 현재 객체가 원래 로그인한 사용자를 나타냅니다. 필드 주석은 ID/PW 로그인을 기준으로 작성되어 있습니다. API Key 인증으로 들어오는 전환 요청은 별도의 인가 매니저에서 차단하므로, 여기서는 일반적인 세션 로그인 이후의 전환 흐름을 중심으로 설명하겠습니다. 예를 들어 관리자 A가 사용자 B로 전환하면 B의 orgLoginUser가 A를 가리킵니다. B 상태에서 사용자 C로 다시 이동하더라도 최초 사용자는 A로 유지됩니다. 전환할 때마다 이전 사용자를 연결하는 이력 구조를 만드는 대신, 언제나 하나의 원본 계정을 기준으로 전환 권한을 판단합니다. Spring Security에도 사용자 전환용 SwitchUserFilter가 있으며, 공식 구현은 SwitchUserGrantedAuthority에 원래 Authentication을 보관해 복귀에 활용합니다. 프로젝트에서는 이 필터를 등록하지 않고, 컨트롤러에서 업무 계층을 검증한 뒤 MyUserDetails.orgLoginUser로 원본 사용자를 보관했습니다. 따라서 다음 코드는 SwitchUserFilter를 확장한 구현이 아니라, 프로젝트의 사용자 모델을 이용한 별도 구현입니다. 3.2. 전환 권한은 현재 사용자보다 최초 사용자를 기준으로 판단합니다 원본 사용자를 보관했다면 전환 요청에서 이 정보를 권한 판단에 사용합니다. 실제 진입점은 POST /api/user/change/{userId}입니다. 컨트롤러는 현재 사용자와 원본 사용자를 구분한 뒤, 원본 사용자의 계층을 기준으로 대상에게 전환할 수 있는지 확인합니다. 코드의 계층 검사는 MyUserDetails의 level을 기준으로 합니다. 1은 SUPER, 2는 ADMIN, 3은 일반 사용자이며, isAdmin()과 isSuper()도 이 값을 검사합니다. ADMIN은 대상의 adminId가 자신의 ID와 같은 경우에만 전환할 수 있습니다. adminId는 일반 사용자에게는 소속 관리자의 ID이고, ADMIN과 SUPER에게는 본인의 ID입니다. 따라서 관리자 A는 자신의 하위 사용자로 이동할 수 있으며, 본인으로 복귀할 때도 같은 조건을 통과합니다. 이 값은 조회 SQL의 case when u.z_mylevel = 3 then u.z_parid else u.z_myid end에서 생성됩니다. SUPER의 전환 요청에는 ADMIN과 같은 대상별 소속 검사 분기가 없습니다. 사용자 계층에 맞는 선택 목록은 제공하지만, 실제 전환 API가 SUPER의 대상을 반드시 그 목록 안으로 제한한다고 설명할 수는 없습니다. 이후에 살펴볼 화면의 전환 후보 목록과 서버의 전환 허용 조건은 구분해야 합니다. 최초 사용자를 기준으로 검사하기 때문에, 이미 일반 사용자로 전환한 상태에서도 org가 관리자를 가리키면 다른 하위 사용자로 이동하거나 관리자 본인으로 돌아갈 수 있습니다. 반면 처음부터 일반 사용자로 로그인했다면 org도 일반 사용자이므로 요청이 거절됩니다. 이 권한 검사는 메서드의 @PreAuthorize가 아니라 메서드 본문에서 수행합니다. 3.3. 인증된 토큰을 새로 만들고 세션에 저장합니다 전환 권한 검사를 통과하고 대상이 확정되면, 해당 사용자를 principal로 가진 새 토큰을 만듭니다. 비밀번호를 다시 검증하는 요청이 아니므로 AuthenticationManager나 ID/PW용 AuthenticationProvider는 호출하지 않습니다. 앞에서 수행한 전환 권한 검사가 이 토큰 생성의 근거가 됩니다. 이 코드에서 권한 적용의 기준은 toBe.authorities입니다. 원래 관리자의 권한을 합치지 않고 대상 사용자의 권한으로 새 Authentication을 구성하므로, 일반 업무 API는 전환된 사용자의 권한을 기준으로 동작합니다. 원래 관리자 정보는 전환과 추적을 위한 참조로 보관되며, 새 토큰에 관리자 권한이 자동으로 승계되는 것은 아닙니다. 권한을 구성한 다음에는 바뀐 인증 상태를 현재 요청과 후속 요청에 반영합니다. 새 SecurityContext를 만들어 SecurityContextHolder에 설정하는 작업은 현재 요청의 실행 주체를 바꿉니다. 이어지는 saveContext는 다음 요청에서도 전환 결과를 사용할 수 있도록 HTTP 세션에 저장합니다. 1장에서 설명한 컨텍스트 설정과 저장의 구분이 여기에도 적용됩니다. 공식 문서에서도 SecurityContextRepository를 후속 요청까지 인증 상태를 연결하는 지점으로 설명하며, HttpSessionSecurityContextRepository는 이를 HTTP 세션에 연결합니다. 이렇게 인증 객체를 교체하면 업무 코드에서도 같은 사용자 조회 방식을 유지할 수 있습니다. 현재 사용자를 가져오는 공통 함수를 통해 이 동작을 확인해 보겠습니다. 전환 후 getCurrent()는 새 principal을 반환합니다. 따라서 이 공통 조회 방식을 사용하는 개별 업무 서비스에 전환 대상의 ID를 별도로 전달할 필요가 없습니다. 현재 사용자로부터 그룹이나 권한을 조회하는 기존 코드도 전환된 사용자 정보를 기준으로 동작합니다. 3.4. 후보 목록과 복귀도 같은 원본 계정을 활용합니다 사용자 전환을 화면에서 제공하려면 이동할 수 있는 후보를 조회하고 원래 계정으로 돌아가는 흐름도 필요합니다. 후보 목록은 GET /api/user/change-list로 조회합니다. 여기에서도 원본 계정을 먼저 구한 다음, 현재 실행 중인 사용자만 목록에서 제외합니다. ADMIN에게는 본인과 직접 하위 사용자를, SUPER에게는 본인과 하위 관리자 및 그 사용자들을 제공합니다. getChildren의 SQL은 z_parid로 직접 하위를 조회하므로, SUPER의 목록은 하위 관리자와 그 사용자에 대한 두 번의 계층 조회로 구성됩니다. 임의 깊이의 트리를 재귀적으로 조회하는 구현은 아닙니다. 목록에 포함된 원본 계정으로 복귀할 때도 같은 전환 API를 사용하며, 별도의 복귀 전용 API는 두지 않았습니다. 원본 사용자 ID를 전달하면 getUserById가 DB에서 새 MyUserDetails를 조회합니다. 대상이 원본과 같을 때는 orgLoginUser를 별도로 설정하지 않으며, 이때 equals는 사용자 ID를 기준으로 비교합니다. 따라서 원본 계정으로 돌아오면 다시 orgLoginUser == null인 상태가 됩니다. 3.5. 실행 사용자와 실제 행위자를 함께 남길 수 있게 합니다 전환과 복귀가 가능해지면, 이후 작업을 누가 수행했는지도 구분할 필요가 있습니다. 사용자 B의 권한으로 실행했다는 사실만 남기면 B가 직접 수행했는지, 관리자 A가 B로 전환해 수행했는지 파악하기 어렵습니다. 이를 위해 감사 로그 모델은 현재 사용자와 원본 사용자를 함께 저장할 수 있도록 구성했습니다. 다음은 감사 로그 생성자 인자 중 관련 부분을 발췌한 코드입니다. 감사 로그 대상 메서드의 AOP는 UserService.getCurrent()로 기록할 사용자를 설정합니다. 따라서 전환 상태에서 기록되는 작업에는 현재 사용자와 원본 사용자를 구분해 담을 수 있습니다. 감사 기록과 함께 확인할 부분은 전환 시 다시 수행하는 인증·세션 처리의 범위입니다. 현재 구현은 컨트롤러에서 인증 객체를 직접 만들기 때문에, 최초 로그인에서 수행하는 계정 상태·허용 IP 검증이나 세션 인증 전략을 다시 호출하지 않습니다. SessionRegistry 재등록, 세션 ID 변경, 로그인 성공 이벤트 발행도 전환 메서드에는 없습니다. 따라서 기존 세션의 인증 주체를 교체하는 처리와 새로운 로그인에 적용하는 동시 세션 관리 정책을 같은 동작으로 보아서는 안 됩니다. 마지막으로 어떤 인증 경로에서 사용자 전환 기능에 접근할 수 있는지 확인해 보겠습니다. ZngApiKeyAuthorizationManager는 "/api/user/change/*" to POST를 금지 목록에 넣고, API Key 자체의 접근 범위와 이 목록을 함께 검사합니다. 따라서 API Key로는 사용자 전환을 할 수 없으며, 컨트롤러뿐 아니라 앞단의 필터 체인에서 수행하는 인가까지 확인해야 전체 제한을 파악할 수 있습니다. 이 글의 사례는 기존 세션 로그인 상태에서 제공하는 사용자 전환 흐름을 중심으로 설명했습니다. 이 구현을 통해서는 원본 계정으로 전환 권한을 판단하고, 대상 계정으로 업무 권한을 구성한 뒤, 바뀐 컨텍스트를 세션에 저장합니다. 이 세 역할을 구분함으로써 일반 업무 API의 현재 사용자 조회 방식을 유지하면서 사용자 전환과 원본 계정 복귀를 구현할 수 있었습니다.
2026.11.04
1