✅ 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에서 설정됩니다:
- 로그인 페이지는 SecurityConfig.java에서 설정됩니다:
.formLogin(form -> form
.loginPage("/auth/login.do") // 로그인 페이지 설정
.loginProcessingUrl("/auth/loginAction.do") // 로그인 처리 URL
.successHandler(customAuthenticationSuccessHandler) // 성공 핸들러
)
- 사용자가 로그인 버튼을 클릭하면 입력된 정보가 /auth/loginAction.do로 전송됩니다.
- 스프링 시큐리티는 이 요청(/auth/loginAction.do)을 가로채서 인증 프로세스를 시작합니다.
- 이때 UsernamePasswordAuthenticationFilter가 동작하여 사용자가 입력한 아이디와 비밀번호를 기반으로 인증 토큰(UsernamePasswordAuthenticationToken)을 생성합니다.
- 생성된 인증 토큰은 AuthenticationManager에게 전달되어 인증이 수행됩니다.
- AuthenticationManager는 내부적으로 DaoAuthenticationProvider를 통해 실제 인증 작업을 처리합니다.
- DaoAuthenticationProvider는 사용자 정보를 로드하기 위해 UserDetailsService(여기서는 MemberDetailService.java)를 호출하여 DB에서 사용자 정보를 조회합니다.
- 조회된 사용자 정보는 MemberDetails 객체로 변환되어 반환됩니다.
- 비밀번호 검증은 PasswordEncoder를 활용하여 사용자가 입력한 비밀번호와 DB에 저장된 비밀번호를 비교하여 수행됩니다.
- 비밀번호가 일치하면 인증이 성공하고, 불일치하면 인증 실패로 처리됩니다.
- 인증에 성공하면:
- 생성된 Authentication 객체가 SecurityContextHolder에 저장됩니다.
- 이후 모든 요청에서 이 정보를 참조할 수 있습니다.
- 설정된 성공 핸들러(CustomAuthenticationSuccessHandler.java)가 호출되어 추가 작업(로그인 이벤트 발행 및 리다이렉트)이 진행됩니다.
- 인증에 실패하면:
- 다시 로그인 페이지(/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)을 전달받아 처리합니다.
동작 과정
- 사용자가 로그인 요청(/auth/loginAction.do)을 보내면, UsernamePasswordAuthenticationFilter가 요청을 가로채고 사용자 입력값(아이디와 비밀번호)을 기반으로 UsernamePasswordAuthenticationToken 객체를 생성합니다.
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
-
- 생성된 토큰은 AuthenticationManager의 authenticate() 메서드로 전달됩니다.
Authentication authentication = authenticationManager.authenticate(token);
-
- AuthenticationManager는 내부적으로 등록된 **DaoAuthenticationProvider**를 통해 인증 작업을 수행합니다.
- DaoAuthenticationProvider는 다음과 같은 작업을 수행합니다:
- UserDetailsService(여기서는 MemberDetailService)를 호출하여 사용자 정보를 로드.
- 비밀번호 검증(PasswordEncoder) 수행.
- 인증 성공 시 새로운 Authentication 객체를 생성하여 반환.
결론
- 토큰 전달(authenticate())은 스프링 시큐리티 내부에서 자동으로 처리됩니다.
- 따라서 개발자가 명시적으로 토큰 전달 코드를 작성하지 않아도 됩니다.
2. UserDetailsService 호출 및 SecurityContextHolder 저장
질문: "UserDetailsService를 호출하여 DB에서 사용자 정보를 조회한다고 했는데, SecurityContextHolder에 MemberDetails(UserDetails)를 저장한다는 의미인가요?"
답변
- 정확히 말하면, SecurityContextHolder에는 인증된 사용자 정보를 포함하는 Authentication 객체가 저장됩니다.
- 이 Authentication 객체의 내부에는 UserDetails(여기서는 MemberDetails)가 포함됩니다.
동작 과정
- UserDetailsService 호출:
- DaoAuthenticationProvider는 인증 과정에서 사용자를 조회하기 위해 UserDetailsService.loadUserByUsername() 메서드를 호출합니다:
-
- 이 메서드는 DB에서 사용자를 조회하고, 사용자 정보를 담은 객체(MemberDetails)를 반환합니다.
- 비밀번호 검증:
- 반환된 MemberDetails 객체의 비밀번호와 사용자가 입력한 비밀번호를 비교하여 검증합니다:
-
- 인증 성공 시 Authentication 생성:
- 비밀번호 검증이 성공하면 새로운 인증 객체(UsernamePasswordAuthenticationToken)가 생성됩니다:
-
- 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로 관리
}
동작 원리
- DaoAuthenticationProvider 등록:
- 이 클래스는 스프링 시큐리티에서 가장 일반적으로 사용되는 인증 제공자입니다.
- 설정된 UserDetailsService와 PasswordEncoder를 사용해 사용자 정보를 로드하고 비밀번호를 검증합니다.
- ProviderManager 생성:
- 여러 개의 인증 제공자(AuthenticationProvider)를 관리할 수 있는 클래스입니다.
- 여기서는 단일 제공자(DaoAuthenticationProvider)만 등록되었습니다.
- 사용 흐름:
- 로그인 요청이 들어오면, 생성된 토큰(UsernamePasswordAuthenticationToken)이 ProviderManager로 전달됩니다.
- ProviderManager는 등록된 DaoAuthenticationProvider에게 토큰을 넘겨 실제 인증 작업을 수행하게 합니다.
최종 정리
- 로그인 요청 → 스프링 시큐리티 필터 체인 → 토큰 생성 및 AuthenticationManager 호출 (authenticate()).
- AuthenticationManager → DaoAuthenticationProvider → UserDetailsService 호출 → 사용자 정보 로드 (MemberDetailService.loadUserByUsername()).
- DaoAuthenticationProvider → PasswordEncoder로 비밀번호 검증.
- 인증 성공 시 새로운 Authentication 객체 생성 → SecurityContextHolder에 저장.
- 이후 요청에서는 SecurityContextHolder에서 인증 정보를 참조하여 처리.
이 흐름에 따라 스프링 시큐리티는 로그인 요청부터 인증 정보 저장까지 모든 과정을 자동으로 처리하며, 개발자는 필요한 부분만 커스터마이징하면 됩니다! 😊
'Spring > 09. spring-security' 카테고리의 다른 글
| Spring Security는 로그인/로그아웃 컨트롤러를 안 만들어도 되는가? (0) | 2025.03.05 |
|---|---|
| Spring Security는 로그인 & 로그아웃을 위한 API인가? (0) | 2025.03.05 |
| AuthenticationManager와 PasswordEncoder (0) | 2025.03.05 |
| .logout()과 .sessionManagement() (0) | 2025.03.05 |
| Spring Security의 보안 구성 흐름 / 설정 설명 (0) | 2025.03.05 |