ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringSecurity 03
    Spring&SpringBoot/SpringSecurity 2025. 9. 17. 18:37

    인증 상태 영속성

    1. SecurityContextRepository & SecurityContextHolderFilter

    SecurityContextRepository

    • 사용자가 인증한 후 요청에 대해 계속 사용자의 인증을 유지하기 위해 사용되는 클래스
    • 인증 정보와 권한이 SecurityContext에 저장되고 HttpSession을 통해 요청 간 영속이 이루어지는 방식
    1. HttpSessionSecurityContextRepository : 요청 간에 HttpSession에 보안 컨텍스트를 저장 / 영속성 유지
    2. RequestAttributeSecurityContextRepository : ServletRequest 에 보안 컨텍스트를 저장 / 영속성 유지 x
    3. NullSecurityContextRepository : 세션을 사용하지 않는 인증(JWT, OAuth2) 일경우 사용  / 영속성 유지 x
    4. 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 값과 비교하여 제어 여부 결정
    1. 사용자 세션 강제 만료 : 최대 허용 개수만큼 동시 인증이 가능하고 그 외 이전 사용자의 세션 만료
    2. 사용자 인증 시도 차단 : 최대 허용 개수만큼 동시 인증이 가능하고 그 외 사용자의 인증 시도 차단

     

     

    @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로 설정된다

     

     

    [세션 생성 정책 전략]

    1. SessionCreationPolicy.ALWAYS
    2. SessionCreationPolicy.NEVER
    3. SessionCreationPolicy.IF_REQUIRED (기본값)
    4. 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
Designed by Tistory.