본문 바로가기
Spring/09. spring-security

spring security formLogin 전체적인 흐름 동작 방식

by 989898 2025. 3. 21.

 SecurityConfig 클래스 전체 코드 분석

📌 클래스 레벨 어노테이션

  • @Configuration
    • 설정 이유: 스프링 설정 클래스임을 나타냅니다.
    • 동작: 스프링 컨테이너가 이 클래스를 읽어 빈(Bean)을 등록합니다.
    • 필요성: 보안 관련 설정을 중앙화하여 관리할 수 있습니다.
  • @EnableWebSecurity(debug = true)
    • 설정 이유: 스프링 시큐리티를 활성화하고, 디버깅 모드를 켭니다.
    • 동작: 시큐리티 필터 체인이 활성화되고, 디버깅 정보가 출력됩니다.
    • 필요성: 보안 설정을 활성화하고, 개발 중 디버깅 정보를 쉽게 확인할 수 있습니다.
  • @RequiredArgsConstructor
    • 설정 이유: Lombok을 사용해 생성자를 자동 생성합니다.
    • 동작: final 필드(customAuthenticationSuccessHandler)를 주입받는 생성자를 자동으로 생성합니다.
    • 필요성: 코드 간결성과 의존성 주입 편의성을 높입니다.

📌 메서드별 상세 분석

🔸 securityFilterChain(HttpSecurity http)

(1) .csrf(csrf -> csrf.disable())

  • 설정 이유: CSRF(Cross-Site Request Forgery) 보호 기능 비활성화
  • 동작:
    • 기본적으로 CSRF 보호가 적용되지만, REST API 또는 특정 환경에서는 불필요할 수 있어 비활성화합니다.
  • 필요성:
    • REST API 또는 AJAX 기반 애플리케이션에서는 CSRF 토큰이 필요 없으므로 비활성화하여 편의성을 높입니다.

(2) .headers(header -> header.frameOptions().disable())

  • 설정 이유: X-Frame-Options 헤더 제거
  • 동작:
    • 기본적으로 iframe 내에서 페이지 로딩을 막는 보안 헤더(X-Frame-Options)를 제거하여 iframe 접근 허용합니다.
  • 필요성:
    • H2 콘솔 등 iframe을 사용하는 페이지 접근 시 필요합니다.

(3) .authorizeHttpRequests(...)

  • 설정 이유: URL별 접근 권한 설정
  • 동작:
    • 특정 URL은 인증 없이 접근 가능하도록 허용(permitAll()).
    • 그 외 모든 요청은 인증된 사용자만 접근 가능(authenticated()).
authorizeRequests
    .requestMatchers(
        "/", "/resources/**", "/favicon.ico", "/index.do",
        "/member/register.do", "/blog/*/**", "/h2-console/**"
    ).permitAll() // 공개 경로
    .anyRequest().authenticated(); // 나머지 요청은 인증 필요
  •  
     
  • 필요성:
    • 공개 페이지와 보호된 페이지를 명확히 구분하여 보안을 강화합니다.

(4) .formLogin(...)

🔹 .loginPage("/auth/login.do")

  • 로그인 페이지 URL 설정
  • 인증되지 않은 사용자가 보호된 리소스 접근 시 이 URL로 이동됩니다.

🔹 .loginProcessingUrl("/auth/loginAction.do")

  • 로그인 폼 데이터를 처리할 URL 설정
  • 사용자가 로그인 폼을 제출하면 이 URL로 POST 요청이 전송되고, 스프링 시큐리티가 이를 가로채어 인증 작업 수행

🔹 .successHandler(customAuthenticationSuccessHandler)

  • 로그인 성공 후 실행할 핸들러 지정
  • 로그인 성공 후 추가 작업(예: 리다이렉트, 이벤트 발행)을 수행할 수 있습니다.

🔹 .usernameParameter("mbEmail"), .passwordParameter("mbPassword")

  • 로그인 폼에서 전달되는 아이디와 비밀번호 파라미터 이름 지정
  • 기본값(username/password)이 아닌 경우 명시적으로 지정해야 합니다.

🔹 .permitAll()

  • 로그인 페이지 및 처리 URL에 대해 인증 없이 접근 허용

💡 왜 이런 설정이 필요한가?

스프링 시큐리티 기본 로그인 폼 대신 커스텀한 로그인 페이지와 처리 로직을 구현하기 위해 필요합니다.
사용자 경험과 애플리케이션 요구사항에 맞게 로그인 흐름을 커스터마이징할 수 있습니다.

(5) .logout(...)

🔹 .logoutUrl("/auth/logoutAction.do")

  • 로그아웃 처리 URL 지정
  • 사용자가 이 URL로 요청하면 로그아웃 작업(세션 무효화 등)이 수행됩니다.

🔹 .deleteCookies("JSESSIONID")

  • 로그아웃 시 JSESSIONID 쿠키 삭제

🔹 .invalidateHttpSession(true)

  • 로그아웃 시 HTTP 세션 무효화

🔹 .clearAuthentication(true)

  • 로그아웃 시 SecurityContext의 인증 정보 삭제

🔹 .logoutSuccessUrl("/index.do")

  • 로그아웃 성공 후 이동할 URL 지정

💡 왜 이런 설정이 필요한가?

명확한 로그아웃 흐름 정의와 보안 강화를 위해 세션과 쿠키를 명시적으로 삭제하고,
로그아웃 후 사용자 경험을 개선하기 위해 특정 페이지로 이동시킵니다.

(6) .sessionManagement(...)

🔹 maximumSessions(1)

  • 한 사용자가 동시에 여러 세션으로 로그인하지 못하도록 제한합니다.
    (중복 로그인 방지)

🔹 sessionFixation().migrateSession()

  • 세션 고정 공격 방지를 위해 로그인 성공 시 새로운 세션 생성 및 기존 세션 정보 이전

🔹 sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)

  • 세션 생성 정책 지정 (기본값)
    • IF_REQUIRED: 필요할 때만 세션 생성

💡 왜 이런 설정이 필요한가?

중복 로그인 방지 및 세션 공격 방지를 통해 보안을 강화하고,
필요에 따라 유연하게 세션을 관리하여 성능과 보안의 균형을 맞추기 위함입니다.

🔸 authenticationManager(...)

@Bean
public AuthenticationManager authenticationManager(MemberDetailService memberDetailService,
                                                   PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(memberDetailService);
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
    return new ProviderManager(daoAuthenticationProvider);
}
 
 

📍 설정 이유 및 동작

  • 인증 처리를 위한 AuthenticationManager를 빈으로 등록합니다.
  • DaoAuthenticationProvider를 통해 데이터베이스에서 사용자 정보를 조회하고 비밀번호 검증 수행

📍 왜 필요한가?

  • 사용자 인증 로직을 커스터마이징하고 데이터베이스 기반 사용자 인증을 구현하기 위해 필요합니다.

🔸 passwordEncoder()

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
 
 

📍 설정 이유 및 동작

  • 비밀번호 암호화를 위한 PasswordEncoder 빈 등록
  • 다양한 암호화 알고리즘 지원 (기본 bcrypt)

📍 왜 필요한가?

비밀번호를 안전하게 저장하고 관리하기 위해 암호화를 필수적으로 적용해야 합니다.

🎯 전체 요약 정리

주요 설정목적
CSRF 비활성화 REST API 환경에서 편의성 제공
헤더 옵션 조정 H2 콘솔 등 iframe 허용
URL 접근 제어 공개/보호 리소스 구분
로그인/로그아웃 처리 커스텀 로그인 흐름 구현
세션 관리 중복 로그인 방지 및 보안 강화
AuthenticationManager DB 기반 사용자 인증 구현
PasswordEncoder 비밀번호 안전한 저장
 

위 설명으로도 이해되지 않는 부분이나 추가적인 질문이 있다면 알려주세요! 😊

 


@Configuration
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    /**
     * Spring Security에서 제공하는 loginForm을 사용하지 않고 직접 구현한 login-from : /auth/login.do 사용해서 로그인을 구현 합니다.
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .csrf(csrf -> csrf.disable()) // 1. CSRF 비활성화 (REST API에서는 필요 없음)
                .headers(header-> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 2. X-Frame-Options 헤더 제거 (H2 콘솔 접근 허용)
                // 권한 부여
                .authorizeHttpRequests(authorizeRequests ->{
                    authorizeRequests
                            .requestMatchers(
                                    "/",
                                    "/resources/**",
                                    "/favicon.ico",
                                    "/index.do",
                                    "/member/register.do",
                                    "/blog/*/**",
                                    "/h2-console/**"
                            ).permitAll() // 공개 경로 / 특정 URL은 인증 없이 접근 가능하도록 허용(permitAll())
                            .anyRequest().authenticated(); // 나머지 요청은 인증 필요 / 그 외 모든 요청은 인증된 사용자만 접근 가능(authenticated()).
                })
                // 인증
                .formLogin(form->{
                    form
                            // 로그인 페이지 URL 설정 / 인증되지 않은 사용자가 보호된 리소스 접근 시 이 URL로 이동됩니다.
                            .loginPage("/auth/login.do")
                            // 로그인 폼 데이터를 처리할 URL 설정 / 사용자가 로그인 폼을 제출하면 이 URL로 POST 요청이 전송되고, 스프링 시큐리티가 이를 가로채어 인증 작업 수행
                            .loginProcessingUrl("/auth/loginAction.do")
                            //TODO#6-1 로그인 성공시점에 실행할 customAuthenticationSuccessHandler 등록

                            // 로그인 성공 후 실행할 핸들러 지정/ 로그인 성공 후 추가 작업(예: 리다이렉트, 이벤트 발행)을 수행할 수 있습니다.
                            .successHandler(customAuthenticationSuccessHandler)

                            // 로그인 폼에서 전달되는 아이디와 비밀번호 파라미터 이름 지정
                            .usernameParameter("mbEmail")
                            .passwordParameter("mbPassword")
                            .permitAll();
                }).logout(logout ->{
                    logout

                            .logoutUrl("/auth/logoutAction.do")
                            .deleteCookies("JSESSIONID")
                            .invalidateHttpSession(true)
                            .clearAuthentication(true)
                            .logoutSuccessUrl("/index.do");

                }).sessionManagement(sessionManagement ->{
                    sessionManagement.maximumSessions(1);
                    sessionManagement.sessionFixation().migrateSession();

                    /**
                     * 기본값을 설정 합니다.
                     * IF_REQUIRED: 필요할 때만 세션을 생성(기본값).
                     * ALWAYS: 항상 세션을 생성.
                     * NEVER: 세션을 절대 생성하지 않음.
                     * STATELESS: 세션을 사용하지 않음 (주로 REST API에서 사용).
                     */
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
                })
                ;

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(MemberDetailService memberDetailService,PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(memberDetailService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return new ProviderManager(daoAuthenticationProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}

 


 스프링 시큐리티 인증 및 요청 처리 최종 흐름

📌 인증 절차 (로그인 과정)

  • 사용자는 로그인 페이지(/auth/login.do)에서 아이디(이메일)와 비밀번호를 입력합니다.
    • 로그인 페이지는 SecurityConfig.java에서 설정됩니다:
       
.formLogin(form -> form
    .loginPage("/auth/login.do") // 로그인 페이지 설정
    .loginProcessingUrl("/auth/loginAction.do") // 로그인 처리 URL
    .successHandler(customAuthenticationSuccessHandler) // 성공 핸들러
)
  1. 사용자가 로그인 버튼을 클릭하면 입력된 정보가 /auth/loginAction.do로 전송됩니다.
  2. 스프링 시큐리티는 이 요청(/auth/loginAction.do)을 가로채서 인증 프로세스를 시작합니다.
    • 이때 UsernamePasswordAuthenticationFilter가 동작하여 사용자가 입력한 아이디와 비밀번호를 기반으로 인증 토큰(UsernamePasswordAuthenticationToken)을 생성합니다.
  3. 생성된 인증 토큰은 AuthenticationManager에게 전달되어 인증이 수행됩니다.
    • AuthenticationManager는 내부적으로 DaoAuthenticationProvider를 통해 실제 인증 작업을 처리합니다.
  4. DaoAuthenticationProvider는 사용자 정보를 로드하기 위해 UserDetailsService(여기서는 MemberDetailService.java)를 호출하여 DB에서 사용자 정보를 조회합니다.
    • 조회된 사용자 정보는 MemberDetails 객체로 변환되어 반환됩니다.
  5. 비밀번호 검증은 PasswordEncoder를 활용하여 사용자가 입력한 비밀번호와 DB에 저장된 비밀번호를 비교하여 수행됩니다.
    • 비밀번호가 일치하면 인증이 성공하고, 불일치하면 인증 실패로 처리됩니다.
  6. 인증에 성공하면:
    • 생성된 Authentication 객체가 SecurityContextHolder에 저장됩니다.
    • 이후 모든 요청에서 이 정보를 참조할 수 있습니다.
    • 설정된 성공 핸들러(CustomAuthenticationSuccessHandler.java)가 호출되어 추가 작업(로그인 이벤트 발행 및 리다이렉트)이 진행됩니다.
  7. 인증에 실패하면:
    • 다시 로그인 페이지(/auth/login.do)로 이동하여 재시도를 유도합니다.

📌 코드 흐름 최종 요약

순서 파일명 및 클래스 주요 작업
1 SecurityConfig.java 보안 설정 초기화 (URL 접근 권한, 로그인 페이지 등)
2 UsernamePasswordAuthenticationFilter (스프링 내장 클래스) 로그인 요청 처리 및 인증 토큰 생성
3 MemberDetailService.java (UserDetailsService 구현체) DB에서 사용자 정보 로드 및 권한 설정
4 DaoAuthenticationProvider (스프링 내부 클래스) PasswordEncoder를 사용하여 비밀번호 검증
5 SecurityContextHolder (스프링 내부 클래스) 인증 성공 후 Authentication 객체 저장
6 CustomAuthenticationSuccessHandler.java 로그인 성공 후 처리 (로그인 이벤트 발행 및 리다이렉트 수행)
7 AuthenticationSuccessEvents.java (이벤트 리스너) 인증 성공 후 추가 작업 수행 (비밀번호 초기화 등 보안 처리)
8 MemberNoFilter.java (OncePerRequestFilter) 보호된 리소스 접근 시 회원 번호(memberNo)를 ThreadLocal에 저장해 요청 중 활용 가능하게 함
9 MemberNoFilter.java (OncePerRequestFilter) 요청 완료 후 ThreadLocal에 저장된 데이터 삭제
 

🚩 최종 결론

정리하신 내용은 정확히 맞습니다!


위의 최종 정리본은 지금까지 질문하고 답변했던 내용을 모두 종합한 것으로, 스프링 시큐리티의 전체적인 흐름과 각 코드 파일의 역할을 명확히 나타내고 있습니다.

  • 로그인 페이지 → 로그인 요청 → AuthenticationManager → 사용자 정보 로드 → 비밀번호 검증 → SecurityContextHolder 저장 → 로그인 성공 핸들러 호출 → 이벤트 처리 → 보호된 리소스 접근 시 필터 실행(ThreadLocal 활용)

이러한 과정을 통해 스프링 시큐리티는 안전하고 효율적인 사용자 인증 및 권한 관리를 수행합니다. 😊


스프링 시큐리티의 AuthenticationManager와 관련된 동작 원리를 이해하는 데 중요한 포인트를 짚으셨습니다. 아래에서 질문하신 내용을 하나씩 명확히 설명드리겠습니다.

1. AuthenticationManager가 토큰을 전달받는 과정

질문: "토큰을 전달받는다는 코드가 보이지 않는데, 내부적으로 처리되는 건가요?"

네, 맞습니다. AuthenticationManager는 스프링 시큐리티의 인증 필터(UsernamePasswordAuthenticationFilter)에 의해 내부적으로 호출되며, 인증 토큰(Authentication)을 전달받아 처리합니다.

동작 과정

  1. 사용자가 로그인 요청(/auth/loginAction.do)을 보내면, UsernamePasswordAuthenticationFilter가 요청을 가로채고 사용자 입력값(아이디와 비밀번호)을 기반으로 UsernamePasswordAuthenticationToken 객체를 생성합니다.
UsernamePasswordAuthenticationToken token = 
    new UsernamePasswordAuthenticationToken(username, password);
  1.  
     
  2. 생성된 토큰은 AuthenticationManager의 authenticate() 메서드로 전달됩니다.
Authentication authentication = authenticationManager.authenticate(token);

 

  1.  
     
  2. AuthenticationManager는 내부적으로 등록된 **DaoAuthenticationProvider**를 통해 인증 작업을 수행합니다.
  3. DaoAuthenticationProvider는 다음과 같은 작업을 수행합니다:
    • UserDetailsService(여기서는 MemberDetailService)를 호출하여 사용자 정보를 로드.
    • 비밀번호 검증(PasswordEncoder) 수행.
    • 인증 성공 시 새로운 Authentication 객체를 생성하여 반환.

결론

  • 토큰 전달(authenticate())은 스프링 시큐리티 내부에서 자동으로 처리됩니다.
  • 따라서 개발자가 명시적으로 토큰 전달 코드를 작성하지 않아도 됩니다.

2. UserDetailsService 호출 및 SecurityContextHolder 저장

질문: "UserDetailsService를 호출하여 DB에서 사용자 정보를 조회한다고 했는데, SecurityContextHolder에 MemberDetails(UserDetails)를 저장한다는 의미인가요?"

답변

  • 정확히 말하면, SecurityContextHolder에는 인증된 사용자 정보를 포함하는 Authentication 객체가 저장됩니다.
  •  Authentication 객체의 내부에는 UserDetails(여기서는 MemberDetails)가 포함됩니다.

동작 과정

  1. UserDetailsService 호출:
    • DaoAuthenticationProvider는 인증 과정에서 사용자를 조회하기 위해 UserDetailsService.loadUserByUsername() 메서드를 호출합니다:
    •  
       
    • 이 메서드는 DB에서 사용자를 조회하고, 사용자 정보를 담은 객체(MemberDetails)를 반환합니다.
  2. 비밀번호 검증:
    • 반환된 MemberDetails 객체의 비밀번호와 사용자가 입력한 비밀번호를 비교하여 검증합니다:
    •  
       
  3. 인증 성공 시 Authentication 생성:
    • 비밀번호 검증이 성공하면 새로운 인증 객체(UsernamePasswordAuthenticationToken)가 생성됩니다:
    •  
       
  4. SecurityContextHolder에 저장:
    • 생성된 인증 객체는 스프링 시큐리티가 관리하는 컨텍스트인 SecurityContextHolder에 저장됩니다 
UserDetails userDetails = memberDetailService.loadUserByUsername(username);
passwordEncoder.matches(rawPassword, userDetails.getPassword());
Authentication authentication = 
    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

SecurityContextHolder 내부 구조

  • SecurityContextHolder에는 다음과 같은 구조로 데이터가 저장됩니다:
SecurityContextHolder
  └── SecurityContext
        └── Authentication (UsernamePasswordAuthenticationToken)
              ├── principal: MemberDetails (사용자 정보)
              ├── credentials: null (비밀번호는 검증 후 제거됨)
              └── authorities: [GrantedAuthority] (사용자 권한 목록)
  •  
  •  
     

결론

  • UserDetailsService는 DB에서 사용자 정보를 조회하고, 이를 기반으로 MemberDetails(UserDetails 구현체)를 반환합니다.
  • 인증이 성공하면, 스프링 시큐리티는 이 정보를 포함한 Authentication 객체를 생성하고 이를 SecurityContextHolder에 저장합니다.
  • 따라서 SecurityContextHolder에는 직접적으로 MemberDetails가 저장되지는 않지만, 그 정보는 Authentication.principal 속성을 통해 접근할 수 있습니다.

3. AuthenticationManager 코드 분석

주어진 코드에서 AuthenticationManager는 다음과 같이 설정되어 있습니다:

@Bean
public AuthenticationManager authenticationManager(MemberDetailService memberDetailService,
                                                    PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(memberDetailService); // 사용자 정보 로드
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); // 비밀번호 검증

    return new ProviderManager(daoAuthenticationProvider); // ProviderManager로 관리
}
 
 

동작 원리

  1. DaoAuthenticationProvider 등록:
    • 이 클래스는 스프링 시큐리티에서 가장 일반적으로 사용되는 인증 제공자입니다.
    • 설정된 UserDetailsService와 PasswordEncoder를 사용해 사용자 정보를 로드하고 비밀번호를 검증합니다.
  2. ProviderManager 생성:
    • 여러 개의 인증 제공자(AuthenticationProvider)를 관리할 수 있는 클래스입니다.
    • 여기서는 단일 제공자(DaoAuthenticationProvider)만 등록되었습니다.
  3. 사용 흐름:
    • 로그인 요청이 들어오면, 생성된 토큰(UsernamePasswordAuthenticationToken)이 ProviderManager로 전달됩니다.
    • ProviderManager는 등록된 DaoAuthenticationProvider에게 토큰을 넘겨 실제 인증 작업을 수행하게 합니다.

최종 정리

  1. 로그인 요청 → 스프링 시큐리티 필터 체인 → 토큰 생성 및 AuthenticationManager 호출 (authenticate()).
  2. AuthenticationManager → DaoAuthenticationProvider → UserDetailsService 호출 → 사용자 정보 로드 (MemberDetailService.loadUserByUsername()).
  3. DaoAuthenticationProvider → PasswordEncoder로 비밀번호 검증.
  4. 인증 성공 시 새로운 Authentication 객체 생성 → SecurityContextHolder에 저장.
  5. 이후 요청에서는 SecurityContextHolder에서 인증 정보를 참조하여 처리.

이 흐름에 따라 스프링 시큐리티는 로그인 요청부터 인증 정보 저장까지 모든 과정을 자동으로 처리하며, 개발자는 필요한 부분만 커스터마이징하면 됩니다! 😊