Spring Security 인증 프로세스 전체 흐름
초기화 → 필터 체인 → 인증 → 인가 — 요청부터 응답까지 완전 해부
목차
- 초기화 과정: DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain
- 필터 체인 전체 순서와 역할
- 인증 아키텍처: AuthenticationManager와 ProviderManager
- 폼 로그인 전체 흐름 (UsernamePasswordAuthenticationFilter)
- 인증 상태 영속성: SecurityContextRepository
- 세션 관리: 고정 보호 · 동시 제어
- 예외 처리: ExceptionTranslationFilter
- 인가 흐름: AuthorizationFilter
- 전체 요청-응답 통합 흐름도
- 메인 다이어그램: 필터 체인 통과 순서와 실패 경로
- 확대 다이어그램: UsernamePasswordAuthenticationFilter 내부
- 조건표: 필터별 통과 조건과 실패 경로
- JWT 비교: 세션 방식 vs JWT 방식 대조
1. 초기화 과정
DelegatingFilterProxy — 서블릿과 Spring의 연결 고리
Tomcat 같은 서블릿 컨테이너는 Spring의 ApplicationContext를 모른다.
DelegatingFilterProxy는 서블릿 필터 인터페이스를 구현하면서,
실제 처리는 Spring Bean인 FilterChainProxy에 위임한다.
이를 통해 Spring Security 필터들이 Spring의 DI 혜택을 받을 수 있다.
AutoConfiguration 초기화 순서
Spring Boot는 SecurityAutoConfiguration과 SpringBootWebSecurityConfiguration을 통해
자동으로 기본 SecurityFilterChain을 구성한다.
// 초기화 핵심 흐름 (요약)
// 1. HttpSecurity.build() 호출 시 각 Configurer가 등록한 필터들을 수집
// 2. FilterOrderRegistration으로 필터 순서 결정
// 3. FilterChainProxy → SecurityFilterChain 목록을 가지고 요청 시 매칭
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 각 DSL 메서드가 내부적으로 Configurer를 등록
http
.csrf(Customizer.withDefaults()) // CsrfConfigurer → CsrfFilter
.sessionManagement(Customizer.withDefaults()) // SessionManagementConfigurer
.formLogin(Customizer.withDefaults()) // FormLoginConfigurer → UsernamePasswordAuthenticationFilter
.exceptionHandling(Customizer.withDefaults()) // ExceptionHandlingConfigurer → ExceptionTranslationFilter
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()); // AuthorizationFilter
return http.build();
}
다중 SecurityFilterChain
// API용 체인 (JWT, STATELESS)
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
// 웹 UI용 체인 (세션, 폼 로그인)
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
2. 필터 체인 전체 순서와 역할
Spring Security의 필터는 고정된 순서로 실행된다. 주요 필터와 역할을 정리한다.
| 필터 | 역할 | 관련 챕터 |
|---|---|---|
DisableEncodeUrlFilter |
URL에 세션 ID가 포함되지 않도록 방지 | Ch.5 |
WebAsyncManagerIntegrationFilter |
비동기 요청에서 SecurityContext 전파 | - |
SecurityContextHolderFilter |
SecurityContext를 SecurityContextRepository에서 로드하고, 요청 후 저장 | Ch.4 |
HeaderWriterFilter |
보안 응답 헤더 추가 (X-Frame-Options, HSTS 등) | - |
CorsFilter |
CORS Preflight 요청 처리 및 응답 헤더 추가 | Ch.7 |
CsrfFilter |
CSRF 토큰 로드 및 상태 변경 요청 검증 | Ch.7 |
LogoutFilter |
로그아웃 요청 처리, 세션/쿠키 무효화 | Ch.2 |
UsernamePasswordAuthenticationFilter |
폼 로그인 처리 (POST /login) | Ch.2, Ch.3 |
DefaultLoginPageGeneratingFilter |
기본 로그인 페이지 HTML 생성 | Ch.2 |
DefaultLogoutPageGeneratingFilter |
기본 로그아웃 페이지 HTML 생성 | Ch.2 |
BasicAuthenticationFilter |
HTTP Basic 인증 처리 | Ch.2 |
RequestCacheAwareFilter |
저장된 요청(RequestCache)으로 현재 요청 교체 | Ch.6 |
SecurityContextHolderAwareRequestFilter |
HttpServletRequest에 보안 메서드(isUserInRole 등) 추가 | - |
AnonymousAuthenticationFilter |
인증 정보가 없으면 Anonymous 토큰을 SecurityContext에 설정 | Ch.2 |
SessionManagementFilter |
인증 후 세션 고정 보호 및 동시 세션 검사 | Ch.5 |
ExceptionTranslationFilter |
이후 필터의 인증·인가 예외를 캐치하여 EntryPoint/Handler로 라우팅 | Ch.6 |
AuthorizationFilter |
요청에 대한 인가 결정 (permitAll/authenticated/hasRole 등) | Ch.8 |
ConcurrentSessionFilter는 동시 세션 제어가 설정된 경우 UsernamePasswordAuthenticationFilter
앞에 추가되며, 만료된 세션의 요청을 차단한다.
3. 인증 아키텍처
핵심 컴포넌트
- Authentication: 인증 요청 정보(principal, credentials) 또는 인증 결과(principal, authorities)를 담는 토큰
- AuthenticationManager: 인증 처리의 진입점 인터페이스 (
authenticate()) - ProviderManager:
AuthenticationManager의 기본 구현체. 여러AuthenticationProvider를 순회하며 처리 가능 여부 확인 - AuthenticationProvider: 특정 Authentication 타입을 처리하는 실제 인증 로직
- UserDetailsService: username으로
UserDetails를 조회하는 인터페이스 - SecurityContext: 인증된 Authentication을 보관하는 컨테이너
- SecurityContextHolder: ThreadLocal로 SecurityContext를 현재 스레드에 바인딩
ProviderManager 위임 흐름
DaoAuthenticationProvider 내부 동작
// DaoAuthenticationProvider 핵심 로직 (요약)
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication) {
// 1. DB에서 사용자 조회
UserDetails loadedUser = this.userDetailsService.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null");
}
return loadedUser;
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) {
// 2. 비밀번호 검증
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException("Bad credentials");
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication,
UserDetails user) {
// 3. 인증 성공 토큰 생성 (credentials 제거, authorities 포함)
UsernamePasswordAuthenticationToken result =
UsernamePasswordAuthenticationToken.authenticated(
principal, authentication.getCredentials(), user.getAuthorities());
return result;
}
}
4. 폼 로그인 전체 흐름
UsernamePasswordAuthenticationFilter 처리 과정
로그인 실패 흐름
LogoutFilter 흐름
// 로그아웃 처리 핵심
// 1. LogoutFilter: POST /logout 요청 매칭
// 2. LogoutHandler 체인 실행:
// - CsrfLogoutHandler: CSRF 토큰 삭제
// - SecurityContextLogoutHandler: SecurityContext 초기화 + 세션 무효화
// - CookieClearingLogoutHandler: 쿠키 삭제
// 3. LogoutSuccessHandler: 리다이렉트
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true) // 세션 무효화
.deleteCookies("JSESSIONID") // 쿠키 삭제
.clearAuthentication(true) // SecurityContext 초기화
);
return http.build();
}
5. 인증 상태 영속성
SecurityContextRepository
인증된 SecurityContext를 요청 간에 유지하는 저장소 인터페이스다.
로그인 성공 후 저장하고, 다음 요청 시 복원한다.
| 구현체 | 저장 위치 | 사용 시나리오 |
|---|---|---|
HttpSessionSecurityContextRepository |
HTTP 세션 | 전통적 세션 기반 웹 앱 (기본값) |
RequestAttributeSecurityContextRepository |
요청 속성 (1회성) | 요청 내에서만 유지 |
NullSecurityContextRepository |
저장 안 함 | STATELESS REST API (JWT 방식) |
DelegatingSecurityContextRepository |
복수 저장소 위임 | 여러 저장 전략을 결합할 때 |
SecurityContextHolderFilter vs SecurityContextPersistenceFilter
Spring Security 6부터 SecurityContextPersistenceFilter는 deprecated되고
SecurityContextHolderFilter가 기본이 되었다.
차이는 저장 시점에 있다.
// SecurityContextHolderFilter 핵심 (요약)
public class SecurityContextHolderFilter extends GenericFilterBean {
private final SecurityContextRepository securityContextRepository;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 지연 로딩 방식으로 SecurityContext 공급자 설정
Supplier deferredContext =
securityContextRepository.loadDeferredContext(httpRequest);
try {
// 2. ThreadLocal에 설정
securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
} finally {
// 3. 요청 완료 후 클리어 (저장은 UsernamePasswordAuthenticationFilter가 담당)
securityContextHolderStrategy.clearContext();
request.removeAttribute(attr);
}
}
}
6. 세션 관리
세션 고정 보호 (Session Fixation Protection)
공격자가 미리 심어놓은 세션 ID를 피해자가 사용하게 만드는 세션 고정 공격을 방어한다. 인증 성공 시 새로운 세션 ID를 발급하는 것이 핵심이다.
| 전략 | 동작 | 권장 |
|---|---|---|
changeSessionId |
세션 ID만 변경 (속성 유지). HttpServletRequest.changeSessionId() 사용 |
기본값 (Servlet 3.1+) |
newSession |
새 세션 생성 (기존 세션 속성 복사 안 함) | 민감한 속성 완전 초기화 필요 시 |
migrateSession |
새 세션 생성 + 기존 속성 이전 | - |
none |
세션 그대로 유지 (보호 없음) | 비권장 |
동시 세션 제어
동일 계정의 중복 로그인을 제어한다. maximumSessions로 허용 세션 수를 제한하고,
초과 시 기존 세션을 만료시키거나 새 로그인을 차단한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.sessionManagement(session -> session
// 세션 고정 보호
.sessionFixation().changeSessionId()
// 동시 세션 제어
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // false: 기존 세션 만료, true: 새 로그인 차단
.expiredUrl("/session-expired")
);
return http.build();
}
// SessionRegistry 빈 등록 (동시 세션 제어 필수)
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
7. 예외 처리
ExceptionTranslationFilter 위치와 역할
필터 체인의 후반부에서 AuthorizationFilter 앞에 위치한다.
자신 이후의 필터에서 발생한 AuthenticationException과 AccessDeniedException을
try-catch로 포착한다.
AuthenticationException은 즉시 AuthenticationEntryPoint를 호출하고,
AccessDeniedException은 먼저 현재 사용자가 익명(Anonymous) 또는 RememberMe 인증인지 확인한다.
익명이나 RememberMe라면 아직 완전한 인증이 아니므로 AuthenticationEntryPoint로 라우팅하여 로그인을 유도하고,
완전히 인증된 사용자라면 AccessDeniedHandler를 호출하여 403 응답을 반환한다.
8. 인가 흐름
AuthorizationFilter (Spring Security 6 기본)
필터 체인의 마지막에 위치하여 요청이 접근 규칙을 충족하는지 검사한다.
AuthorizationManager가 실제 인가 결정을 담당한다.
// 인가 규칙 설정 예시
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**", "/login", "/css/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN
.requestMatchers(HttpMethod.POST, "/api/**").hasAuthority("api:write")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
);
9. 전체 요청-응답 통합 흐름도
미인증 사용자가 보호된 리소스에 접근하는 경우
로그인 후 원래 URL로 복귀하는 경우
인증된 사용자의 일반 요청
전체 흐름 핵심 정리
- 초기화: DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain 빌드. AutoConfiguration이 기본 필터와 규칙을 구성한다.
- CsrfFilter: 상태 변경 요청(POST/PUT/DELETE)마다 토큰을 검증한다. GET은 검증 생략.
- SecurityContextHolderFilter: 매 요청마다 세션에서 인증 컨텍스트를 복원하고, 응답 후 클리어한다.
- ConcurrentSessionFilter: 만료된 세션이면 로그아웃 처리 후 만료 URL로 리다이렉트한다.
- UsernamePasswordAuthenticationFilter: POST /login 요청에서 인증 토큰 생성 → ProviderManager → DaoAuthenticationProvider → UserDetailsService 순서로 인증한다.
- AnonymousAuthenticationFilter: SecurityContext가 비어있으면 Anonymous 토큰을 설정한다.
- ExceptionTranslationFilter: AuthorizationFilter의 예외를 캐치. AuthenticationException은 즉시 EntryPoint로, AccessDeniedException은 익명/RememberMe 여부를 확인하여 EntryPoint 또는 AccessDeniedHandler로 라우팅.
- AuthorizationFilter: 접근 규칙에 따라 GRANT/DENY 결정. DENY 시 예외를 던진다.
- RequestCache: 인증 전 요청 URL을 세션에 저장했다가 로그인 성공 후 원래 URL로 복귀시킨다.
- 세션 고정 보호: 로그인 성공 시 세션 ID를 변경하여 세션 고정 공격을 방어한다.
10. 메인 다이어그램 — 필터 체인 통과 순서와 실패 경로
HTTP 요청이 Spring Security를 통과하는 전체 순서다. 정상 흐름은 위에서 아래로, 실패 흐름은 빨간 점선으로 표시했다.
11. 확대 다이어그램 — UsernamePasswordAuthenticationFilter 내부
POST /login 요청이 매칭되었을 때 필터 내부에서 일어나는 일을 단계별로 확대한 다이어그램이다.
각 단계에서 실패 시 던지는 예외를 함께 표시했다.
12. 조건표 — 필터별 통과 조건과 실패 경로
| 필터 | 하는 일 | 통과 조건 | 실패 시 이동 경로 / 예외 |
|---|---|---|---|
SecurityContextHolderFilter |
세션에서 SecurityContext 복원, 요청 후 클리어 | 항상 통과 (복원할 컨텍스트 없으면 빈 컨텍스트) | 실패 없음 |
HeaderWriterFilter |
보안 응답 헤더 추가 (X-Frame-Options, HSTS 등) | 항상 통과 | 실패 없음 |
CsrfFilter |
CSRF 토큰 검증 | GET/HEAD/OPTIONS 요청이거나 토큰 유효 | AccessDeniedException → 403 |
LogoutFilter |
로그아웃 요청 처리 | 로그아웃 URL(POST /logout)이 아니면 통과 | 로그아웃 처리 후 리다이렉트 (필터 체인 중단) |
UsernamePasswordAuthenticationFilter |
폼 로그인 인증 처리 | POST /login이 아니면 통과, 인증 성공 시 SuccessHandler | BadCredentialsException 등 → FailureHandler → /login?error |
AnonymousAuthenticationFilter |
SecurityContext 비어있으면 Anonymous 토큰 설정 | 항상 통과 (이미 인증됐으면 스킵) | 실패 없음 |
SessionManagementFilter |
세션 고정 보호, 동시 세션 검사 | 세션 정책 위반 아닌 경우 | SessionAuthenticationException → 세션 만료 URL |
ExceptionTranslationFilter |
이후 필터의 보안 예외를 try-catch로 포착 | 예외 없으면 통과 | 예외 캐치 후 EntryPoint(401) 또는 AccessDeniedHandler(403)로 라우팅 |
AuthorizationFilter |
접근 규칙 검사 (permitAll, authenticated, hasRole 등) | 접근 규칙 충족 | AuthenticationException → 401 / AccessDeniedException → 403 |
ConcurrentSessionFilter는 동시 세션 제어 설정 시 추가되며,
SessionRegistry에서 세션 만료 여부를 확인한다. 만료된 세션이면 로그아웃 처리 후 만료 URL로 리다이렉트.
13. JWT 비교 — 세션 방식 vs JWT 방식 대조
세션 방식과 JWT 방식의 핵심 차이는 인증 상태를 어디에 저장하느냐다.
JWT 방식에서는 세션을 사용하지 않으므로 세션 관련 필터가 비활성화되고,
커스텀 JWT 필터가 UsernamePasswordAuthenticationFilter 앞에 삽입된다.
세션 방식 필터 체인
JWT 방식 필터 체인
설정 차이 비교
| 항목 | 세션 방식 | JWT 방식 |
|---|---|---|
| 인증 상태 저장 | 서버 세션 (HttpSession) | 클라이언트 토큰 (Authorization 헤더) |
| SecurityContextRepository | HttpSessionSecurityContextRepository |
NullSecurityContextRepository |
| 세션 생성 정책 | IF_REQUIRED (기본값) |
STATELESS |
| CSRF | 활성화 (세션 쿠키 보호) | 비활성화 (토큰이 자동 전송되지 않으므로 CSRF 불필요) |
| 폼 로그인 | formLogin() 활성화 |
비활성화 (별도 로그인 API) |
| 커스텀 필터 | 없음 | JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 앞에 추가 |
JWT 필터 삽입 위치
@Bean
public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/auth/signup").permitAll()
.anyRequest().authenticated()
)
// ★ 핵심: JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 삽입
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
JWT 필터 내부 동작
SecurityContextHolder에 Authentication을 저장하는 것은 동일하다.
차이는 저장 위치가 세션이 아니라 요청 스코프(ThreadLocal)에만 존재한다는 점이다.
매 요청마다 JWT에서 새로 파싱하여 설정하고, 요청 종료 시 클리어된다.
'Java & Spring > Spring Security' 카테고리의 다른 글
| 07. 악용 보호 (0) | 2026.08.06 |
|---|---|
| 06. 예외 처리 (0) | 2026.08.05 |
| 05. 세션 관리 (0) | 2026.08.05 |
| 04. 인증 상태 영속성 (0) | 2026.08.04 |
| 03. 인증 아키텍처 (0) | 2026.08.04 |