Java & Spring/Spring Security

08. 인증 프로세스 전체 흐름

승주우에요 2026. 8. 6. 09:01
인증 프로세스 전체 흐름 총 정리 - Spring Security

Spring Security 인증 프로세스 전체 흐름

초기화 → 필터 체인 → 인증 → 인가 — 요청부터 응답까지 완전 해부

Ch.1 초기화 Ch.2 인증 프로세스 Ch.3 인증 아키텍처 Ch.4 인증 상태 영속성 Ch.5 세션 관리 Ch.6 예외 처리 Ch.7 악용 보호
Spring Security는 서블릿 필터 체인에 통합된 보안 프레임워크다. 모든 HTTP 요청은 DelegatingFilterProxy를 통해 Spring 컨텍스트로 진입하고, FilterChainProxy가 적절한 SecurityFilterChain으로 라우팅한다. 필터들은 순서대로 CSRF 검증 → 인증 상태 복원 → 로그인 처리 → 익명 처리 → 예외 감지 → 인가 검사를 수행한다. 이 문서는 챕터 1~7의 내용을 하나의 흐름으로 연결하여 Spring Security의 전체 그림을 이해할 수 있도록 정리한다.

1. 초기화 과정

DelegatingFilterProxy — 서블릿과 Spring의 연결 고리

Tomcat 같은 서블릿 컨테이너는 Spring의 ApplicationContext를 모른다. DelegatingFilterProxy는 서블릿 필터 인터페이스를 구현하면서, 실제 처리는 Spring Bean인 FilterChainProxy에 위임한다. 이를 통해 Spring Security 필터들이 Spring의 DI 혜택을 받을 수 있다.

flowchart LR REQ([HTTP 요청]) --> SC[서블릿 컨테이너\nFilter Chain] SC --> DFP[DelegatingFilterProxy\n서블릿 필터] DFP -->|Spring Bean 위임| FCP[FilterChainProxy\nspringSecurityFilterChain] FCP --> SFC1[SecurityFilterChain #1\n/api/**] FCP --> SFC2[SecurityFilterChain #2\n/**] SFC1 --> FILTERS1[보안 필터들...] SFC2 --> FILTERS2[보안 필터들...] classDef servlet fill:#2d333b,stroke:#768390,color:#e6edf3 classDef spring fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef chain fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef filter fill:#1a3a2a,stroke:#3fb950,color:#ffffff class SC,DFP servlet class FCP spring class SFC1,SFC2 chain class FILTERS1,FILTERS2 filter

AutoConfiguration 초기화 순서

Spring Boot는 SecurityAutoConfigurationSpringBootWebSecurityConfiguration을 통해 자동으로 기본 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 위임 흐름

flowchart TD FILTER[UsernamePasswordAuthenticationFilter] -->|인증되지 않은 토큰| PM[ProviderManager] PM -->|supports 확인| P1[DaoAuthenticationProvider] PM -->|supports 확인| P2[OAuth2LoginAuthenticationProvider] PM -->|supports 확인| PN[...추가 Provider] P1 -->|지원 가능| UDS[UserDetailsService\n.loadUserByUsername] UDS --> UD[UserDetails 반환] UD --> PE[PasswordEncoder 검증] PE -->|성공| AT[인증된 Authentication 반환] PM -->|부모로 위임| PPM[Parent ProviderManager\n글로벌 설정] AT --> CTX[SecurityContext에 저장] CTX --> SH[SuccessHandler 호출] classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef manager fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef provider fill:#5c3d00,stroke:#d29922,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef neutral fill:#2d333b,stroke:#768390,color:#e6edf3 class FILTER filter class PM,PPM manager class P1,P2,PN,UDS,UD,PE provider class AT,CTX,SH success

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 처리 과정

sequenceDiagram participant U as 사용자 participant CF as CsrfFilter participant LF as UsernamePasswordAuthenticationFilter participant PM as ProviderManager participant DAP as DaoAuthenticationProvider participant UDS as UserDetailsService participant SMF as SessionManagementFilter participant SH as SuccessHandler / FailureHandler U->>CF: POST /login (username, password, _csrf) CF->>CF: CSRF 토큰 검증 CF->>LF: chain.doFilter() LF->>LF: requiresAuthentication() → /login POST 확인 LF->>LF: UsernamePasswordAuthenticationToken(미인증) 생성 LF->>PM: authenticate(token) PM->>DAP: authenticate(token) DAP->>UDS: loadUserByUsername(username) UDS-->>DAP: UserDetails 반환 DAP->>DAP: PasswordEncoder.matches() 검증 DAP-->>PM: 인증된 Authentication 반환 PM-->>LF: 인증된 Authentication 반환 LF->>SMF: SessionAuthenticationStrategy 실행\n(세션 고정 보호 + 동시 세션 검사) LF->>LF: SecurityContextHolder에 Authentication 저장 LF->>LF: SecurityContextRepository.saveContext() LF->>SH: successfulAuthentication() SH->>U: Redirect (SavedRequest URL 또는 defaultTargetUrl)

로그인 실패 흐름

sequenceDiagram participant U as 사용자 participant LF as UsernamePasswordAuthenticationFilter participant PM as ProviderManager participant FH as SimpleUrlAuthenticationFailureHandler U->>LF: POST /login (잘못된 비밀번호) LF->>PM: authenticate(token) PM-->>LF: BadCredentialsException LF->>LF: SecurityContextHolder.clearContext() LF->>FH: onAuthenticationFailure() FH->>U: Redirect → /login?error

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가 기본이 되었다. 차이는 저장 시점에 있다.

flowchart LR subgraph "SecurityContextHolderFilter (Spring Security 6 기본)" direction TB A1[요청 진입] --> B1[SecurityContextRepository.loadDeferredContext] B1 --> C1[SecurityContextHolder에 설정] C1 --> D1[다음 필터 처리] D1 --> E1[SecurityContextHolder 클리어\n저장은 명시적으로 호출한 필터가 담당] end classDef step fill:#1c3a5f,stroke:#58a6ff,color:#ffffff class A1,B1,C1,D1,E1 step
// 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로 허용 세션 수를 제한하고, 초과 시 기존 세션을 만료시키거나 새 로그인을 차단한다.

flowchart TD LOGIN[로그인 요청] --> CSF[ConcurrentSessionControlAuthenticationStrategy] CSF --> CHECK{현재 세션 수\n< maximumSessions?} CHECK -->|예: 여유 있음| REG[SessionRegistry에 등록\n→ 로그인 성공] CHECK -->|아니오: 한도 초과| STRATEGY{maxSessionsPreventsLogin} STRATEGY -->|false 기본값| EXPIRE[가장 오래된 세션 만료\n→ 새 로그인 허용] STRATEGY -->|true| BLOCK[새 로그인 차단\n SessionAuthenticationException] EXPIRE --> CSF2[ConcurrentSessionFilter\n다음 요청 시 만료 감지] CSF2 --> REDIRECT[로그아웃 처리 후\n만료 URL로 리다이렉트] classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef decision fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef fail fill:#5c1a1a,stroke:#f85149,color:#ffffff class CSF,CSF2 filter class CHECK,STRATEGY decision class REG,EXPIRE success class BLOCK,REDIRECT fail
@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 앞에 위치한다. 자신 이후의 필터에서 발생한 AuthenticationExceptionAccessDeniedException을 try-catch로 포착한다. AuthenticationException은 즉시 AuthenticationEntryPoint를 호출하고, AccessDeniedException은 먼저 현재 사용자가 익명(Anonymous) 또는 RememberMe 인증인지 확인한다. 익명이나 RememberMe라면 아직 완전한 인증이 아니므로 AuthenticationEntryPoint로 라우팅하여 로그인을 유도하고, 완전히 인증된 사용자라면 AccessDeniedHandler를 호출하여 403 응답을 반환한다.

flowchart TD ETF[ExceptionTranslationFilter] -->|try 블록| AUTHZ[AuthorizationFilter] AUTHZ -->|예외 없음| PASS([정상 처리]) AUTHZ -->|예외 발생| CATCH{예외 유형 분류} CATCH -->|AuthenticationException| AE_FLOW[SecurityContext 초기화\nRequestCache 저장\nAuthenticationEntryPoint 호출] CATCH -->|AccessDeniedException| AD_CHECK{"isAnonymous() ?\nisRememberMe() ?"} AD_CHECK -->|"Yes (익명 또는 RememberMe)"| AE_FLOW AD_CHECK -->|"No (완전 인증된 사용자)"| AD_FLOW[AccessDeniedHandler 호출\n403 응답] AE_FLOW --> REDIRECT["/login 리다이렉트\n또는 401 JSON 응답"] AD_FLOW --> FORBIDDEN["/access-denied 리다이렉트\n또는 403 JSON 응답"] classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef decision fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef auth fill:#5c3d00,stroke:#d29922,color:#ffffff classDef fail fill:#5c1a1a,stroke:#f85149,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff class ETF,AUTHZ filter class CATCH,AD_CHECK decision class AE_FLOW,REDIRECT auth class AD_FLOW,FORBIDDEN fail class PASS success

8. 인가 흐름

AuthorizationFilter (Spring Security 6 기본)

필터 체인의 마지막에 위치하여 요청이 접근 규칙을 충족하는지 검사한다. AuthorizationManager가 실제 인가 결정을 담당한다.

flowchart LR REQ([요청]) --> AF[AuthorizationFilter] AF --> AM[AuthorizationManager] AM --> RULES{접근 규칙 매칭} RULES -->|permitAll| ALLOW([허용]) RULES -->|authenticated\n+ 인증됨| ALLOW RULES -->|authenticated\n+ 미인증| AE[AuthenticationException\n→ ExceptionTranslationFilter] RULES -->|"hasRole/hasAuthority\n+ 권한 있음"| ALLOW RULES -->|"hasRole/hasAuthority\n+ 권한 없음"| ADE[AccessDeniedException\n→ ExceptionTranslationFilter] classDef filter fill:#3d1f5c,stroke:#bc8cff,color:#ffffff classDef decision fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef success fill:#1a3a2a,stroke:#3fb950,color:#ffffff classDef fail fill:#5c1a1a,stroke:#f85149,color:#ffffff class AF,AM filter class RULES decision class ALLOW success class AE,ADE fail
// 인가 규칙 설정 예시
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. 전체 요청-응답 통합 흐름도

미인증 사용자가 보호된 리소스에 접근하는 경우

sequenceDiagram participant U as 사용자 (미인증) participant DFP as DelegatingFilterProxy participant FCP as FilterChainProxy participant CSRF as CsrfFilter participant SCHF as SecurityContextHolderFilter participant ANO as AnonymousAuthenticationFilter participant ETF as ExceptionTranslationFilter participant AUTHZ as AuthorizationFilter participant RC as RequestCache (Session) participant AEP as AuthenticationEntryPoint U->>DFP: GET /my-page DFP->>FCP: 위임 FCP->>CSRF: CSRF 토큰 로드 (GET이므로 검증 생략) CSRF->>SCHF: chain.doFilter() SCHF->>SCHF: SecurityContextRepository에서 컨텍스트 로드\n(세션 없음 → EmptyContext) SCHF->>ANO: chain.doFilter() ANO->>ANO: SecurityContext가 비어있음\n→ AnonymousAuthenticationToken 설정 ANO->>ETF: chain.doFilter() ETF->>AUTHZ: chain.doFilter() [try 블록 내] AUTHZ->>AUTHZ: /my-page → authenticated() 규칙 확인\nAnonymous → InsufficientAuthenticationException AUTHZ-->>ETF: AuthenticationException 발생 ETF->>ETF: SecurityContext 초기화 ETF->>RC: saveRequest("/my-page") ETF->>AEP: commence() 호출 AEP-->>U: Redirect → /login

로그인 후 원래 URL로 복귀하는 경우

sequenceDiagram participant U as 사용자 participant CSRF as CsrfFilter participant SCHF as SecurityContextHolderFilter participant LF as UsernamePasswordAuthenticationFilter participant PM as ProviderManager participant SMF as SessionAuthenticationStrategy participant SH as SavedRequestAwareAuthenticationSuccessHandler participant RC as RequestCache (Session) U->>CSRF: POST /login (_csrf, username, password) CSRF->>CSRF: CSRF 토큰 검증 CSRF->>SCHF: chain.doFilter() SCHF->>LF: chain.doFilter() LF->>LF: /login POST 매칭 LF->>PM: authenticate(UsernamePasswordAuthenticationToken) PM-->>LF: 인증된 Authentication LF->>SMF: 세션 고정 보호 (changeSessionId) LF->>LF: SecurityContextHolder.setAuthentication() LF->>SCHF: SecurityContextRepository.saveContext() LF->>SH: onAuthenticationSuccess() SH->>RC: getRequest() → "/my-page" 복원 SH-->>U: Redirect → /my-page

인증된 사용자의 일반 요청

sequenceDiagram participant U as 사용자 (인증됨) participant CSRF as CsrfFilter participant SCHF as SecurityContextHolderFilter participant CONC as ConcurrentSessionFilter participant ETF as ExceptionTranslationFilter participant AUTHZ as AuthorizationFilter participant APP as 애플리케이션 (Controller) U->>CSRF: GET /my-page (JSESSIONID 쿠키) CSRF->>SCHF: chain.doFilter() SCHF->>SCHF: 세션에서 SecurityContext 로드\n→ Authentication 복원 SCHF->>CONC: chain.doFilter() CONC->>CONC: 세션 만료 여부 확인 (만료 아님) CONC->>ETF: chain.doFilter() ETF->>AUTHZ: chain.doFilter() [try 블록] AUTHZ->>AUTHZ: /my-page → authenticated() 확인\n인증된 사용자 → GRANT AUTHZ->>APP: DispatcherServlet → Controller APP-->>U: 200 OK (응답)

전체 흐름 핵심 정리

  • 초기화: 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를 통과하는 전체 순서다. 정상 흐름은 위에서 아래로, 실패 흐름은 빨간 점선으로 표시했다.

flowchart TD REQ([HTTP 요청]) --> DFP["DelegatingFilterProxy\n서블릿 → Spring 위임"] DFP --> FCP["FilterChainProxy\nURL 패턴으로 SecurityFilterChain 선택"] FCP --> SFC["SecurityFilterChain 진입"] SFC --> F1["SecurityContextHolderFilter\n세션에서 SecurityContext 복원"] F1 --> F2["HeaderWriterFilter\n보안 응답 헤더 추가"] F2 --> F3["CsrfFilter\nCSRF 토큰 검증 (POST/PUT/DELETE)"] F3 -->|"GET 또는 토큰 유효"| F4["LogoutFilter\nPOST /logout 매칭 시 로그아웃 처리"] F3 -..->|"토큰 불일치"| CSRF_FAIL["AccessDeniedException\n→ 403 Forbidden"]:::fail F4 -->|"로그아웃 아님"| F5["UsernamePasswordAuthenticationFilter\nPOST /login 매칭 시 인증 처리"] F4 -..->|"로그아웃 요청"| LOGOUT_END["세션 무효화 + 쿠키 삭제\n→ /login?logout 리다이렉트"]:::fail F5 -->|"로그인 URL 아니면 통과"| F6["AnonymousAuthenticationFilter\nSecurityContext 비어있으면 Anonymous 토큰 설정"] F5 -..->|"인증 실패"| AUTH_FAIL["BadCredentialsException\n→ /login?error 리다이렉트"]:::fail F5 -..->|"인증 성공"| AUTH_OK["SecurityContext 저장 + 세션 고정 보호\n→ SuccessHandler 리다이렉트"]:::success F6 --> F7["SessionManagementFilter\n세션 고정 보호 + 동시 세션 검사"] F7 --> F8["ExceptionTranslationFilter\ntry 블록으로 이후 필터 감싸기"] F8 --> F9["AuthorizationFilter\n접근 규칙 검사 (permitAll/authenticated/hasRole)"] F9 -->|"GRANT"| APP(["DispatcherServlet → Controller\n200 OK"]):::success F9 -..->|"AuthenticationException"| EX_AUTH["SecurityContext 초기화\nRequestCache 저장\n→ AuthenticationEntryPoint (401)"]:::fail F9 -..->|"AccessDeniedException"| EX_CHECK{"isAnonymous?\nisRememberMe?"} EX_CHECK -..->|"Yes"| EX_AUTH EX_CHECK -..->|"No (완전 인증)"| EX_DENIED["AccessDeniedHandler\n→ 403 Forbidden"]:::fail classDef default fill:#161b22,stroke:#30363d,color:#c9d1d9 classDef fail fill:#3d1117,stroke:#f85149,color:#ffa198 classDef success fill:#0a2e1a,stroke:#3fb950,color:#7ee787 linkStyle 4,7,9,13,14,16 stroke:#f85149,stroke-dasharray:5 5 linkStyle 10 stroke:#3fb950
점선 = 실패 경로. CsrfFilter에서 토큰 불일치로 튕기거나, AuthorizationFilter에서 예외가 발생하면 ExceptionTranslationFilter가 캐치하여 EntryPoint 또는 AccessDeniedHandler로 라우팅한다.

11. 확대 다이어그램 — UsernamePasswordAuthenticationFilter 내부

POST /login 요청이 매칭되었을 때 필터 내부에서 일어나는 일을 단계별로 확대한 다이어그램이다. 각 단계에서 실패 시 던지는 예외를 함께 표시했다.

flowchart TD START(["POST /login 요청 매칭"]) --> TOKEN["UsernamePasswordAuthenticationToken 생성\n(미인증 상태: principal=username, credentials=password)"] TOKEN --> AM["AuthenticationManager.authenticate(token)"] AM --> PM["ProviderManager\nAuthenticationProvider 목록 순회"] PM --> SUPPORTS{"provider.supports()\n토큰 타입 지원?"} SUPPORTS -->|"지원 안 함"| NEXT["다음 Provider 시도"] NEXT --> SUPPORTS SUPPORTS -..->|"모든 Provider 실패"| PROV_EX["ProviderNotFoundException"]:::fail SUPPORTS -->|"지원함 (DaoAuthenticationProvider)"| UDS["UserDetailsService\n.loadUserByUsername(username)"] UDS -..->|"사용자 없음"| UNF_EX["UsernameNotFoundException\n(BadCredentialsException으로 변환)"]:::fail UDS -->|"UserDetails 반환"| PRE_CHECK["PreAuthenticationChecks\n계정 잠김/비활성/만료 확인"] PRE_CHECK -..->|"계정 문제"| LOCK_EX["LockedException\nDisabledException\nAccountExpiredException"]:::fail PRE_CHECK -->|"통과"| PE["PasswordEncoder.matches()\n입력 비밀번호 vs 저장된 해시"] PE -..->|"불일치"| BAD_EX["BadCredentialsException"]:::fail PE -->|"일치"| POST_CHECK["PostAuthenticationChecks\n자격증명 만료 확인"] POST_CHECK -..->|"자격증명 만료"| CRED_EX["CredentialsExpiredException"]:::fail POST_CHECK -->|"통과"| AUTH_TOKEN["인증된 Authentication 생성\n(principal=UserDetails, authorities=권한 목록,\ncredentials=null)"]:::success AUTH_TOKEN --> SESSION["SessionAuthenticationStrategy 실행\n(세션 고정 보호 + 동시 세션 검사)"] SESSION --> CTX["SecurityContextHolder에 Authentication 저장\nSecurityContextRepository.saveContext()"] CTX --> HANDLER["AuthenticationSuccessHandler 호출\n→ SavedRequest URL 또는 defaultTargetUrl로 리다이렉트"]:::success UNF_EX --> FAIL_HANDLER["AuthenticationFailureHandler 호출\n→ /login?error 리다이렉트"]:::fail BAD_EX --> FAIL_HANDLER LOCK_EX --> FAIL_HANDLER CRED_EX --> FAIL_HANDLER PROV_EX --> FAIL_HANDLER classDef default fill:#161b22,stroke:#30363d,color:#c9d1d9 classDef fail fill:#3d1117,stroke:#f85149,color:#ffa198 classDef success fill:#0a2e1a,stroke:#3fb950,color:#7ee787 linkStyle 5,8,10,12,14 stroke:#f85149,stroke-dasharray:5 5

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 앞에 삽입된다.

세션 방식 필터 체인

flowchart LR subgraph "세션 방식 (Stateful)" direction LR S1["SecurityContext\nHolderFilter\n세션에서 복원"] --> S2["CsrfFilter\n토큰 검증"] S2 --> S3["UsernamePassword\nAuthenticationFilter\n폼 로그인"] S3 --> S4["SessionManagement\nFilter\n세션 고정 보호"] S4 --> S5["ExceptionTranslation\nFilter"] S5 --> S6["AuthorizationFilter"] end classDef active fill:#1c3a5f,stroke:#58a6ff,color:#ffffff class S1,S2,S3,S4,S5,S6 active

JWT 방식 필터 체인

flowchart LR subgraph "JWT 방식 (Stateless)" direction LR J1["SecurityContext\nHolderFilter\n빈 컨텍스트"] --> J2["JwtAuthenticationFilter\n★ 커스텀 필터\nAuthorization 헤더에서\nJWT 파싱 + 검증"] J2 --> J3["ExceptionTranslation\nFilter"] J3 --> J4["AuthorizationFilter"] end classDef active fill:#1c3a5f,stroke:#58a6ff,color:#ffffff classDef custom fill:#5c3d00,stroke:#d29922,color:#ffffff class J1,J3,J4 active class J2 custom

설정 차이 비교

항목 세션 방식 JWT 방식
인증 상태 저장 서버 세션 (HttpSession) 클라이언트 토큰 (Authorization 헤더)
SecurityContextRepository HttpSessionSecurityContextRepository NullSecurityContextRepository
세션 생성 정책 IF_REQUIRED (기본값) STATELESS
CSRF 활성화 (세션 쿠키 보호) 비활성화 (토큰이 자동 전송되지 않으므로 CSRF 불필요)
폼 로그인 formLogin() 활성화 비활성화 (별도 로그인 API)
커스텀 필터 없음 JwtAuthenticationFilterUsernamePasswordAuthenticationFilter 앞에 추가

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 필터 내부 동작

flowchart TD REQ(["요청 진입"]) --> HEADER{"Authorization 헤더\n존재?"} HEADER -->|"없음"| SKIP["필터 통과\n→ AnonymousAuthenticationToken"] HEADER -->|"Bearer xxx"| PARSE["JWT 파싱 + 서명 검증"] PARSE -..->|"만료/변조"| JWT_FAIL["JwtException\n→ 401 Unauthorized"]:::fail PARSE -->|"유효"| CLAIMS["Claims에서 username, authorities 추출"] CLAIMS --> AUTH["UsernamePasswordAuthenticationToken.authenticated() 생성"] AUTH --> CTX["SecurityContextHolder에 저장\n(세션 저장 없음)"] CTX --> NEXT(["다음 필터로 진행"]):::success classDef default fill:#161b22,stroke:#30363d,color:#c9d1d9 classDef fail fill:#3d1117,stroke:#f85149,color:#ffa198 classDef success fill:#0a2e1a,stroke:#3fb950,color:#7ee787 linkStyle 3 stroke:#f85149,stroke-dasharray:5 5
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