-
SpringSecurity 03Spring&SpringBoot/SpringSecurity 2025. 9. 17. 18:37
인증 상태 영속성
1. SecurityContextRepository & SecurityContextHolderFilter
SecurityContextRepository
- 사용자가 인증한 후 요청에 대해 계속 사용자의 인증을 유지하기 위해 사용되는 클래스
- 인증 정보와 권한이 SecurityContext에 저장되고 HttpSession을 통해 요청 간 영속이 이루어지는 방식
- HttpSessionSecurityContextRepository : 요청 간에 HttpSession에 보안 컨텍스트를 저장 / 영속성 유지
- RequestAttributeSecurityContextRepository : ServletRequest 에 보안 컨텍스트를 저장 / 영속성 유지 x
- NullSecurityContextRepository : 세션을 사용하지 않는 인증(JWT, OAuth2) 일경우 사용 / 영속성 유지 x
- DelegatingSecurityContextRepository : RequestAttributeSecurityContextRepository 와 HttpSessionSecurityContextRepository 를 동 시에 사용할 수 있도록 위임된 클래스로서 초기화 시 기본으로 설정됨
SecurityContextHolderFilter
- SecurityContextRepository 사용하여 SecurityContext를 얻고 이를 SecurityContextHolder에 설정하는 필터 클래스
- 사용자가 명시적으로 호출해야 SecurityContext를 저장 가능
- SecurityContextRepository에서 SecurityContext를 로드하고 응답 시점에 session에 SecurityContext 저장하지 않는다
- 인증이 지속되어야 하는지를 각 인증 매커니즘이 독립적으로 선택할 수 있게 하여 더 나은 유연성을 제공하고 HttpSession에 필요할 때만 저장함으로써 성능 향상
SecurityContext 생성, 저장, 삭제
1. 익명 사용자
- SecurityContextRepository를 사용하여 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장 후 다음 필터로 전달
- AnonymousAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext 에 저장
2. 인증 요청
- SecurityContextRepository를 사용하여 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장 후 다음 필터로 전달
- UsernamePasswordAuthenticationFilter에서 인증 성공 후 SecurityContext에 sernamePasswordAuthenticationToken 객체를 SecurityContext 에 저장
- SecurityContextRepository를 사용하여 HttpSession에 SecurityContext 저장
3. 인증 후 요청
- SecurityContextRepository를 사용하여 HttpSession에서 SecurityContext 껴내서 SecurityContextHolder에 저장 후 다음 필터로 전달
- SecurityContext 안에 Authentication 객체가 존재하면 인증 유지
4. 클라이언트 응답 시 공통
- SecurityContextHolder.clearContext()로 컨텍스트 삭제 (스레드 풀의 스레드일 경우 반드시 필요)
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class); AuthenticationManager authenticationManager = builder.build(); http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/login").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) // 방법 1. // .securityContext(securityContext -> securityContext.requireExplicitSave(false)) .authenticationManager(authenticationManager) .addFilterBefore(customAuthenticationFilter(http, authenticationManager), UsernamePasswordAuthenticationFilter.class) ; return http.build(); } public CustomAuthenticationFilter customAuthenticationFilter(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(http); customAuthenticationFilter.setAuthenticationManager(authenticationManager); return customAuthenticationFilter; } @Bean public UserDetailsService userDetailsService() { return new CustomUserDetailsService(); } }
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final ObjectMapper objectMapper = new ObjectMapper(); public CustomAuthenticationFilter(HttpSecurity http) { super(PathPatternRequestMatcher .withDefaults() .matcher(HttpMethod.GET, "/api/login")); setSecurityContextRepository(getSecurityContextRepository(http)); } // 방법 2. private SecurityContextRepository getSecurityContextRepository(HttpSecurity http) { SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); if (securityContextRepository == null) { securityContextRepository = new DelegatingSecurityContextRepository( new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository()); } return securityContextRepository; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); return this.getAuthenticationManager().authenticate(token); } }
2. 스프링 MVC 인증 구현
- Spring Security 필터에 의존하는 대신 수동으로 사용자를 인증하는 경우 MVC 컨트롤러 엔드포인트 사용 가능
- 요청 간 인증을 저장하려면 HttpSessionSecurityRepository 사용하여 인증 상태 저장
### Send POST request with json body POST https://localhost:8080/login Content-Type: application/json { "username": "user", "password": "1111" }
@RestController @RequiredArgsConstructor public class LoginController { private final AuthenticationManager authenticationManager; private final HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository(); @PostMapping("/login") public Authentication login(@RequestBody LoginRequest login, HttpServletRequest request, HttpServletResponse response) { // 인증 되지 않은 인증 객체 생성 UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(login.getUsername(), login.getPassword()); // 인증 시도 후 최종 인증 결과 반환 Authentication auth = authenticationManager.authenticate(token); // 인증 결과 context, threadLocal 저장 SecurityContext securityContext = SecurityContextHolder.getContextHolderStrategy().createEmptyContext(); securityContext.setAuthentication(auth); SecurityContextHolder.getContextHolderStrategy().setContext(securityContext); // 컨텍스트를 세션에 저장해서 인증 상태 영속 httpSessionSecurityContextRepository.saveContext(securityContext, request, response); return auth; } }
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/login").permitAll() .anyRequest().authenticated()) .csrf(AbstractHttpConfigurer::disable) ; return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public UserDetailsService userDetailsService() { return new CustomUserDetailsService(); } }
세션 관리
1. 동시 세션 제어 - sessionMangement().maximumSession()
- 동시 세션 제어는 사용자가 동시에 여러 세션을 생성하는 것을 관리하는 전략
- 사용자 인증 후 활성화된 세션의 수가 설정된 maximumSessions 값과 비교하여 제어 여부 결정
- 사용자 세션 강제 만료 : 최대 허용 개수만큼 동시 인증이 가능하고 그 외 이전 사용자의 세션 만료
- 사용자 인증 시도 차단 : 최대 허용 개수만큼 동시 인증이 가능하고 그 외 사용자의 인증 시도 차단
@RestController public class IndexController { ... @GetMapping("/invalidSessionUrl") public String invalidSessionUrl() { return "invalidSessionUrl"; } @GetMapping("/expiredUrl") public String expiredUrl() { return "expiredUrl"; } }
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/invalidSessionUrl", "/expiredUrl").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .sessionManagement(session -> session .invalidSessionUrl("/invalidSessionUrl") .maximumSessions(1) .maxSessionsPreventsLogin(false) .expiredUrl("/expiredUrl")) ; return http.build(); } }
2. 세션 고정 보호 - sessionMangement().sessionFixation()
- 세선 고정 공격은 악의적인 공격자가 사이트에 접근하여 세션을 생성한 다음 다른 사용자가 같은 세션으로 로그인하도록 유도하는 위험
- Spring Security는 사용자가 로그인할 때 새로운 세션을 생성하거나 세션 ID를 변경함으로써 공격에 자동 대응
[세션 고정 보호 전략]
- changeSessionId() : 기존 세션 유지하면서 세션 ID만 변경해 인증 과정에서 세션 고정 공격을 방지하는 방식 (기본값)
- newSession() : 새로운 세션을 생성하고 기존 세션 데이터를 복사하지 않는 방식
- migrationSession() : 새로운 세션을 생성하고 모든 기존 세션 속성을 새 세션으로 복사
- none() : 기존 세션 그대로 사용
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/invalidSessionUrl", "/expiredUrl").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .sessionManagement(session -> session .sessionFixation(sesionFixation -> sesionFixation.changeSessionId()) ) ; return http.build(); } }
3. 세션 생성 정책 - sessionMangement().sessionCreationPolicy()
- 인증된 사용자에 대한 세션 생성 정책을 설정하여 어떻게 세션을 관리할지 결정할 수 있으며 이 정책은 SessionCreationPolicy로 설정된다
[세션 생성 정책 전략]
- SessionCreationPolicy.ALWAYS
- SessionCreationPolicy.NEVER
- SessionCreationPolicy.IF_REQUIRED (기본값)
- SessionCreationPolicy.STATELSS
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/invalidSessionUrl", "/expiredUrl").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) ; return http.build(); } }
4. SessionMangementFilter / ConcurrentSessionFilter
SessionMangementFilter
- 요청이 시작된 이후 사용자가 인증되었는지 감지하고 인증된 경우 세션 고정 보호 메커니즘을 활성화하거나 동시 다중 로그인을 확인하는 등 세션 관련 활동을 수행하기 위해 설정된 세션 인증 전략을 호출하는 필터 클래스
- SessionManagementFilter가 기본적으로 설정되지 않으며 세션관리 API 통해 생성
ConcurrentSessionFilter
- 각 요청에 대해 SessionRegistry에서 SessionInformation을 검색하고 세션이 만료로 표시되었는지 확인하고 만료로 표시된 경우 로그아웃 처리 무효화(세션 무효화)
- 각 요청에 대해 SessionRegistry.refreshLastRequest(String) 호출하여 등록된 세션들이 항상 마지막 업데이트 날짜/시간 갖도록 한다
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/invalidSessionUrl", "/expiredUrl").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(true) ) ; return http.build(); } }
@RestController @RequiredArgsConstructor public class IndexController { private final SessionInfoService sessionInfoService; @GetMapping("/sessionId") public String sessionInfo() { sessionInfoService.sessionInfo(); return "sessionInfo"; } ... }
@EnableWebSecurity @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/invalidSessionUrl", "/expiredUrl").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .sessionManagement(session -> session .maximumSessions(2) .maxSessionsPreventsLogin(false) ) ; return http.build(); } @Bean SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } }
@Service @RequiredArgsConstructor public class SessionInfoService { private final SessionRegistry sessionRegistry; public void sessionInfo() { List<Object> allPrinciples = sessionRegistry.getAllPrincipals(); for (Object principle : allPrinciples) { List<SessionInformation> allSession = sessionRegistry.getAllSessions(principle, false); for (SessionInformation sessionInfo : allSession) { System.out.println("시용자: " + principle + ", 세션ID : " + sessionInfo.getSessionId() + ", 최종 요청 시간 : " + sessionInfo.getLastRequest()); } } } }
728x90'Spring&SpringBoot > SpringSecurity' 카테고리의 다른 글
SpringSecurity 04 (0) 2025.09.18 SpringSecurity 02 (0) 2025.09.16 SpringSecurity 01 (0) 2025.09.15