
Spring Security 의 인증 알아보기
안녕하세요. 네이버페이 회원&인증BE 의최용화입니다. Spring Security는 강력한 보안 프레임워크로서, 애플리케이션의 인증과 인가 과정을 효율적으로 관리합니다. 저희 팀에서는 다음과 같은 기능을 구현하는 데에 Spring Security 를 사용하고있습니다. 인증 여부 확인 (인증이 안된 사용자일 경우 로그인 및 가입 유도 / 인증이 완료된 사용자일 경우 적절한 권한 부여), 권한을 이용한 접근 제어(권한이 없는 사용자일 경우 권한 획득을 위한 절차 수행), 보안 공격으로부터 보호(CSRF 공격 방지), PC / MOBILE 최적화 페이지 제공(사용자가 접근한 환경을 파악하여 최적화된 페이지제공) 이 글에서는 Spring Security의 인증(Authentication) 과정 전반을 살펴보고, 각 단계의 역할과 작동 방식을 자세히알아보겠습니다. 이 글은 Spring Security 6.3.0 공식문서를 기반으로작성되었습니다. Spring Security 의 Filter 기반 동작 방식이해 Spring Security의 인증 수행을 이해하려면 Spring Security 의 구조에 대한 이해가 선행되어야 합니다. Spring Security는 Servlet Filter 기반으로 동작합니다. 여기서, 중요한 개념인 FilterChainProxy, SecurityFilterChain, 보안 필터(Security Filter)에 대해알아봅니다. FilterChainProxy의 개념 FilterChainProxy는 Spring Security에서 제공하는 특수한 Filter로 SecurityFilterChain을 사용하여 다양한 보안 필터가 동작하게합니다. 사실, Servlet Container의 라이프사이클과 Spring의 ApplicationContext 사이를 연결하는 DelegatingFilterProxy 라는 필터 구현체도 중요한 상위 개념이나 이 글에서는 설명을 생략합니다. 자세한 설명은 이 페이지를참고하세요. SecurityFilterChain의 개념 SecurityFilterChain은 Spring Security에서 보안 필터(Security Filter)의 체인을 정의하는 데 사용됩니다. 요청이 애플리케이션의 Servlet에 도달하기 전에 다양한 보안 검사를 수행하는 필터들이 있으며, 이를 보안 필터라고 부릅니다. SecurityFilterChain은 각 보안 필터가 순차적으로 실행되도록 하여 애플리케이션의 보안 설정을 체계적으로 관리할 수 있게합니다. 보안 필터(Security Filter)의 주요기능 SecurityFilterChain 에 선언된 다양한 보안 필터를 통해 아래의 기능을 수행하게됩니다. 인증(Authentication): 사용자의 신원을 확인합니다. 예를 들어, 사용자가 로그인 폼을 제출하면, 이를 처리하는 필터가실행됩니다. 인가(Authorization): 사용자가 요청한 리소스에 접근할 권한이 있는지확인합니다. 각종 보안 공격으로부터 보호(Protection Against Exploits): CSRF 공격, Session Fixation 공격, sniffing 공격, Clickjacking 등의 보안 공격으로부터보호합니다. 세션 관리: 사용자의 세션을 생성, 관리, 종료하는과정입니다. 기타 기능: HTTP 응답 헤더를 설정하여 보안을 강화하는 기능, Remember Me 기능 등을지원합니다. 보안 필터(Security Filter)소개 여러 개의 보안 필터가 있지만, 이 글에서 자주 보게 될 몇 가지 보안 필터만 가볍게 소개 드리려고 합니다. 나열된 순서대로실행됩니다. UsernamePasswordAuthenticationFilter: 폼 기반 로그인 처리를수행합니다. DefaultLoginPageGeneratingFilter: 기본 로그인 페이지를생성합니다. ExceptionTranslationFilter: ExceptionTranslationFilter 의 다음 Filter 에서 발생한 Exception을 처리하고 이에 대한 적절한 응답을반환합니다. AuthorizationFilter: 사용자가 요청한 리소스에 대해 접근 권한이 있는지 확인합니다. 권한이 없는 경우 접근을 거부하고, 적절한 에러 페이지를 반환하거나 예외를발생시킵니다. Spring Security의 Form 기반인증 인증(Authentication)은 특정 리소스에 액세스하려는 주체(Principal)의 신원을 확인하는 과정입니다. Spring Security는 다양한 인증 방법을 지원하며, 이 글에서는 주로 폼 기반 인증(Form-Based Authentication)을 예로 들어설명하겠습니다. 인증 관련 주요용어 Spring Security 인증과 관련하여 자주 사용되는 용어에 대해설명합니다. SecurityContextHolder: Spring Security가 인증된 사용자의 정보를 저장하는곳입니다. SecurityContext: SecurityContextHolder에서 가져오며 현재 인증된 사용자의 인증정보(Authentication)를 포함합니다. Authentication: 사용자가 입력한 자격 증명(Pricipal과 Credentials)을 AuthenticationManager에 전달하는 용도로 사용되거나 SecurityContext에서 현재 사용자를 나타내는 용도로 사용되는객체입니다. GrantedAuthority(Authorities): 인증된 사용자에게 부여된 권한을 나타내며, 역할(role)이나 범위(scope) 등을포함합니다. AuthenticationManager: Spring Security의 필터가 인증을 수행하는 방법을 정의한 API(인터페이스)입니다. ProviderManager: AuthenticationManager 의구현체입니다. AuthenticationProvider: ProviderManager가 여러 종류의 인증(Basic 인증, Form 인증 등)을 지원 및 수행하기 위해 사용하는 인터페이스입니다. 하나의 ProviderManager에 여러 개의 AuthenticationProvider를 등록하여 사용할 수 있습니다. 가장 흔히 사용되는 구현체는 DaoAuthenticationProvider입니다. Form 기반 인증 수행 과정 — UsernamePasswordAuthenticationFilter 아래는 UsernamePasswordAuthenticationFilter 에서 수행하는 인증 과정에 대한도식입니다. Form 기반 인증 요청에서 username과 password를 추출하여 UsernamePasswordAuthenticationToke 객체를 ProviderManager에 전달합니다. (여기서, UsernamePasswordAuthenticationToken은 위에서 설명한 Authentication 인터페이스의 구현체이고, ProviderManager는 위에서 설명한 AuthenticationManager 인터페이스의구현체입니다.) ProviderManager는 DaoAuthenticationProvider 를 이용하여 인증을 수행합니다. (여기서, DaoAuthenticationProvider 는 위에서 설명한 AuthenticationProvider 인터페이스의구현체입니다.) DaoAuthenticationProvider는 UserDetailsService를 이용해 전달받은 username과 일치하는 UserDetails(저장된 사용자 정보)를조회합니다. DaoAuthenticationProvider는 PasswordEncoder를 이용해 전달받은 password와 3번 과정에서 조회한 UserDetails의 비밀번호가 일치하는지검증합니다. 4번 과정에서 비밀번호 검증까지 성공하면 사용자 인증은 성공한 것입니다. 이 때, 인증이 완료된 UsernamePasswordAuthenticationToken 을 반환하게 되며 이 구현체의 principal 값은 UserDetailsService에서 조회해온 UserDetails로 설정됩니다. 최종적으로, 반환된 UsernamePasswordAuthenticationToken은 SecurityContextHolder에 설정됩니다. Form 기반 인증 수행 예시 — 예제코드 인증 수행 과정을 Spring Security 예제 코드와 TRACE 로깅을 통해확인해봅니다. 먼저, Gradle 의존성부터설정합니다. dependencies ( implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' ) 리소스를 관리하는 ResourceController.java 입니다. @Controller public class ResourceController ( @GetMapping("/private") @ResponseBody public String loginSuccess() ( return "Private Resource" ) ) 우리가 보호하려는 리소스에 대한 핸들러를 간단하게명시하였습니다. 다음으로 Spring Security 에서 제공하는 Form 기반 인증을 구성한 SecurityConfig.java 입니다. @EnableWebSecurity @Configuration public class SecurityConfig ( @Bean public UserDetailsService userDetailsService() ( InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager() UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password(�") .authorities("READ") .build() inMemoryUserDetailsManager.createUser(user) return inMemoryUserDetailsManager ) @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception ( http.authorizeHttpRequests(authorize -> ( authorize .requestMatchers("/private").hasAuthority("READ") .anyRequest().authenticated() )) http.formLogin(Customizer.withDefaults()) return http.build() ) ) 이 구성에서 설정한 내용은 아래와같습니다. 사용자 정보 저장을 위해 메모리 저장소를 이용하며 사용자 정보를 추가해두었습니다. 사용자의 정보는 다음과같습니다. username: user password: 12345 권한: READ 2. 보호할 리소스인 http://localhost:8080/private 리소스에 접근하기 위해서는 인증된 사용자여야 하며, 인증된 사용자가 가지는 권한 중 READ 권한이 있어야 함을명시하였습니다. 3. Spring Security 에서 기본적으로 제공하는 Form 기반 인증을수행합니다. 다음으로 Spring Security 에서 제공하는 로깅 기능을 명시한 application.properties 파일입니다. logging.level.org.springframework.security=TRACE Security Filter가 수행되는 순서와 사용자 요청이 어떻게 처리되는지 로그를 통해 확인할 수있습니다. Form 기반 인증 수행 예시 — 인증되지 않은 사용자가 리소스를 요청할때 흐름도 인증되지 않은 사용자가 http://localhost:8080/private 리소스에 접근을시도합니다. AuthorizationFilter에서 AccessDeniedException을 발생시켜 인증되지 않은 요청이 거부되었음을알립니다. ExceptionTranslationFilter는 AuthorizationFilter 에서 발생한 AccessDeniedException 에 대한 처리로 아래의 과정을수행합니다. SecurityContextHolder 에 저장된 Authentication 데이터가지워집니다. 추후에 인증 과정이 성공할 때, 현재 실패한 http://localhost:8080/private 요청을 바로 수행할 수 있도록 현재의 요청 정보가 담긴 HttpServletRequest 객체를 RequestCache에 저장해둡니다. AuthenticationEntryPoint에 구현된 인증되지 않은 사용자에게 자격 증명을 요청하는 기능을 수행합니다. 여기서 구현된 AuthenticationEntryPoint 객체는 LoginUrlAuthenticationEntryPoint 이므로 로그인 페이지(기본 설정 값은 http://localhost:8080/login)로 redirect 하는 작업을 수행하게됩니다. 4. 사용자의 브라우저는 redirect된 로그인 페이지 (기본 설정 값은 http://localhost:8080/login) 를 요청하게됩니다. 5. LoginController 에서 로그인 페이지(login.html)를 렌더링하여응답합니다. 로그 확인 위 로그는 흐름도 상 1 ~ 2번까지의 과정에 대한 로그입니다. 인증되지 않은 사용자가 http://localhost:8080/private 요청을 호출할 때 발생하는 로그입니다. 로그를 통해 아래와 같은 사실을 알 수있습니다. FilterChainProxy를 통해 15개의 보안 필터(Security Filter)가수행됩니다. 8번째 보안 필터로 UsernamePasswordAuthenticationFilter가 수행되지만, 로그인 요청(POST /login)이 아니기 때문에 실질적인 인증 과정은 수행되지 않습니다. Spring Security 내부 구현은 아래와같습니다. UsernamePasswordAuthenticationFilter — 로그인 요청(POST /login) 이 아닐 경우, 인증 로직을 수행하지 않고 다음 보안 필터호출 UsernamePasswordAuthenticationFilter — 로그인 요청(POST /login) 인지확인 15번째 보안 필터로 AuthorizationFilter가 수행되지만, /private 리소스에 접근하려는 사용자가 인증되지 않은 사용자이기 때문에 AccessDeniedException 을던집니다. 14번째 보안 필터인 ExceptionTranslationFilter 는 15번째 보안 필터인 AuthorizationFilter에서 던지는 AcessDeniedException 을 잡아 이전 절에서 설명한 예외 처리 로직을 수행합니다. Spring Security 내부 구현은 아래와같습니다. ExceptionTranslationFilter — 다음 보안 필터 수행 중 예외 발생 시, handleSpringSecurityException 메소드호출 handleSpringSecurityException 메소드 — 발생한 예외가 AccessDeniedException 일 경우, handleAceessDeniedException 메소드호출 handleAceessDeniedException 메소드 — 인증 정보가 AnonymousAuthentication이므로 sendStartAuthentication 메소드호출 SecurityContext 제거 / 기존 요청 저장 / 로그인 진입점으로이동 위 로그는 흐름도 상 3 ~ 5번까지의 과정에 대한로그입니다. 로그를 통해 아래와 같은 사실을 알 수있습니다. 14번째 보안 필터인 ExceptionTranslationFilter 에 의해 아래 작업이수행됩니다. 현재의 요청 정보가 담긴 HttpServletRequest 객체를 HttpSessionRequestCache에 저장해둡니다. AuthenticationEntryPoint 에 의해 로그인 페이지(http://localhost:8080/login)로 redirect 하게됩니다. 사용자가 인증을 위해 로그인 페이지(http://localhost:8080/login)를 요청하면, 9번째 보안 필터인 DefaultLoginPageGeneratingFilter 에 의해 기본 설정된 로그인 페이지를 응답합니다. Spring Security 내부 구현은 아래와같습니다. DefaultLoginPageGeneratingFilter — 기본 로그인 페이지 생성 및응답 Form 기반 인증 수행 예시 — 인증 과정에서 실패할때 흐름도 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 HttpServletRequest 객체에서 username과 password를 추출하여 Authentication의 구현체인 UsernamePasswordAuthenticationToken을 생성합니다. 다음으로 UsernamePasswordAuthenticationToken이 인증을 위해 AuthenticationManager 인스턴스로전달됩니다. 인증이 실패하면 아래 과정을수행합니다. SecurityContextHolder 에 저장된 Authentication 데이터가지워집니다. RememberMeServices.loginFail() 메소드가 호출됩니다. RememberMeService 를 설정하지 않은 경우, 어떤 작업도 수행되지 않습니다. 이 예제에서는 RememberMeService 기능을 별도로 설정하지 않았기 때문에 어떤 작업도 수행되지않습니다. AuthenticationFailureHandler 에 구현된 onAuthenticationFailure() 메소드를 수행합니다. 기본적으로 설정되어 있는 AuthenticationFailureHandler의 구현체는 SimpleUrlAuthenticationFailureHandler 입니다. 인증이 실패하면 /login?error URL로 redirect합니다. 로그인 페이지에서는 error 파라미터의 값을 사용하여 인증 실패 메시지를 사용자에게 표시할 수있습니다. 로그 확인 위 로그는 흐름도 상 1 ~ 2번까지의 과정에 대한로그입니다. 인증되지 않은 사용자가 POST http://localhost:8080/login 요청을 잘못된 인증 정보와 함께 전송 시, 발생하는 로그입니다. 로그를 통해 아래와 같은 사실을 알 수있습니다. 8번째 보안 필터로 UsernamePasswordAuthenticationFilter가 수행되고 ProviderManager 와 DaoAuthenticationProvider 가 순차적으로 수행되며 인증이 수행됩니다. Spring Security 내부 구현은 아래와같습니다. UsernamePasswordAuthenticationFilter — AuthenticationManager 의 authenticate 메소드호출 AuthenticationManager 의 authenticate 메소드 — AuthenticationProvider의 authenticate 메소드호출 AuthenticationProvider의 authenticate 메소드 — 제출된 username과 일치하는 사용자 정보가 없어 BadCredentialException 메소드호출 위 로그는 흐름도 상 3번 과정에 대한 로그입니다. 로그를 통해 아래와 같은 사실을 알 수있습니다. 인증 실패 시, 아래 작업을수행합니다. SecurityContextHolder 에 저장된 Authentication 데이터가지워집니다. SimpleUrlAuthenticationFailureHandler 에 구현된 onAuthenticationFailure() 메소드를 수행합니다. /login?error URL로 redirect합니다. Spring Security 내부 구현은 아래와같습니다. AuthenticationFailureHan
