본문 바로가기

Spring Boot/Security 쿠키,세션 기반 인증,인가

세션 관리 핵심 컴포넌트 : SecurityContextHolder 와 SecurityContext

 - Spring Security에서 세션 관리의 중심이 되는 SecurityContextHolder 와 SecurityContext 의 역할

 - 스레드 로컬(ThreadLocal) 을 통한 인증 정보 관리 방식

 - SecurityContext 구조와 내부에 어떤 인증 정보(Authentication) 가 저장되는지 알기

 - 실무 예제로 SecurityContext 가 어떻게 사용되는지 알기

 - 동시 요청이나 비동기 상황에서 발생할 수 있는 문제점들을 이해하고, 올바른 사용법 알기


SecurityContextHolder 와 Security Context

1. SecurityContextHolder 의 역할

Authentication 처리 흐름, Authorization 아님

 

  • SecurityContextHolder 는 Security에서 현재 인증(Authentication) 정보를 저장하고 조회하는 중앙 저장소 역할을 한다
    그림에서 맨 위에 있는게 SecurityContextHolder 임
  • 기본적으로 스레드 로컬(ThreadLocal) 방식을 사용하여 각 요청마다 인증 정보를 독립적으로 보관한다

로그인 시 생성된 Authentication 객체는 SecurityContextHolder 에 저장되고, 이후의 요청 처리 과정에서 언제든지 참조 가능

 

 

 

2. SecurityContext 의 구조

  • SecurityContext 는 실제 인증 정보인 Authentication 객체를 담고 있는 컨테이너

구조

package org.springframework.security.core.context;

import java.io.Serializable;
import org.springframework.security.core.Authentication;

public interface SecurityContext extends Serializable {
    Authentication getAuthentication();

    void setAuthentication(Authentication authentication); // 핵심
}

 

setAuthentication  메서드에 주입되는 Authentication 객체가 핵심이다

사용자 정보, 권한, 인증 여부 등이 담겨있음

 

구성 요소 설명
Principal 사용자 정보(보통 UserDetails 구현체)
Credentials 비밀번호 또는 인증 토큰(보통 인증 후에는 제거됨)
Authorities 사용자가 가진 권한(Role) 목록
Details 부가 정보(IP, 세션 ID 등)

 


SecurityContextHolder 동작 방식

 

SecurityContextHolder 의 일부

package org.springframework.security.core.context;

import java.lang.reflect.Constructor;
import java.util.function.Supplier;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public class SecurityContextHolder {
	// 실제 저장 방식을 얘가 결정함
    private static SecurityContextHolderStrategy strategy;
    private static String strategyName = System.getProperty("spring.security.strategy");
	
        private static void initializeStrategy() {
        if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
            Assert.state(strategy != null, 
            "When using MODE_PRE_INITIALIZED, 
            setContextHolderStrategy must be called with the fully constructed strategy");
        } else {
            if (!StringUtils.hasText(strategyName)) {
                strategyName = "MODE_THREADLOCAL";
            }
			
            // 각 요청 스레드마다 별도의 SecurityContext를 저장한다
            if (strategyName.equals("MODE_THREADLOCAL")) {
                strategy = new ThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
                strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_GLOBAL")) {
                strategy = new GlobalSecurityContextHolderStrategy();
            } else {
                try {
                    Class<?> clazz = Class.forName(strategyName);
                    Constructor<?> customStrategy = clazz.getConstructor();
                    strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
                } catch (Exception var2) {
                    Exception ex = var2;
                    ReflectionUtils.handleReflectionException(ex);
                }

            }
        }
    }
    
    public static void clearContext() { // 얘
        strategy.clearContext();
    }

    public static SecurityContext getContext() { // 얘
        return strategy.getContext();
    }

    public static void setContext(SecurityContext context) { // 얘
        strategy.setContext(context);
    }
}

 

Spring Security는 기본적으로 ThreadLocal 을 사용하여 각 요청 스레드마다 별도의 SecurityContext 를 저장한다

이렇게 하면 멀티스레드 환경에서 인증 정보가 뒤섞이지 않고, 요청 단위로 분리된다고 함

 

요청이 끝나면 clearContext() 메서드가 호출되어 메모리 누수를 방지한다

 

 

 

2. 전략 모드(Strategy) 변경

  • SecurityContextHolder 는 기본적으로 MODE_THREADLOCAL 전략을 사용한다
            if (strategyName.equals("MODE_THREADLOCAL")) {
                strategy = new ThreadLocalSecurityContextHolderStrategy();

 

  • 필요하다면 다른 전략도 사용할 수 있다
전략 설명
MODE_THREADLOCAL 기본값
요청 스레드마다 독립된 SecurityContext 저장
MODE_INHERITABLETHREADLOCAL 자식 스레드가 부모 스레드의 SecurityContext를 상속함
MODE_GLOBAL 모든 스레드가 하나의 SecurityContext를 공유함
(거의 사용하지 않음)

 

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

예제

1. SecurityContext 에서 사용자 조회하기

UserController.java

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @GetMapping("/me")
    public String me() {
        // SecurityContextHolder는 Authentication 정보를 저장하고 조회하는 기능
        // 실제로는 SecurityContext에 저장된다
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "현재 사용자는 " + authentication.getName() + "이고, 권한은 " + authentication.getAuthorities();
    }
}

 

로그인한 사용자가 /me API를 호출하면,

SecurityContextHolder 에 저장된 인증 정보를 통해 사용자의 이름과 권한을 확인할 수 있다

 

/me 를 호출할 시점에 SecurityFilterChain 에 anyRequest.permitAll() 와 같이 뚫어놨다면 anonymousUser 라고 나온다

 

 

2. 커스텀 서비스에서 인증 정보 활용하기

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void placeOrder() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        System.out.println(username + "님의 주문이 접수되었습니다.");
    }
}

 

별도의 인증 객체 전달 없이도, 전역에서 SecurityContextHolder 를 통해 사용자의 정보를 확인할 수 있어서 매우 편리하다

 

 

여기에 사용된 FilterChain

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    @Bean
    @Order(0)
    public SecurityFilterChain h2FilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher(PathRequest.toH2Console())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .anyRequest().authenticated())
                .csrf(csrf ->csrf.disable())
                .headers(headers -> headers
                        .frameOptions(frame -> frame.sameOrigin()))
                .formLogin(login -> login
                        .loginPage("/login").permitAll());
        return http.build();
    }

    @Bean
    @Order(1)
    public SecurityFilterChain meFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/me/**")
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/me/**").authenticated()
                        .anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults()); // Basic Auth
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user")
                .password("{noop}pass")
                .roles("USER")
                .build();

        UserDetails admin = User.withUsername("admin")
                .password("{noop}pass")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}

 

 


  • 항상 요청이 끝나면 SecurityContext 가 정리(clearContext)되는지 확인해야한다. 그렇지 않으면 메모리 누수가 발생할 수 있다
  • 비동기처리(@Async) 에서는 기본적으로 ThreadLocal 이 전파되지 않기 때문에,
    DelegatingSecurityContextRunnable 등을 사용해야한다고 한다
  • Authentication 객체에는 비밀번호와 같은 민감한 정보들이 포함될 수 있으므로
    인증 후 즉시 제거(eraseCredentials) 하거나, 민감한 정보를 포함시키지 말아야 한다

개념 핵심 포인트
SecurityContextHolder 인증 정보를 전역적으로 보관 / 조회하는 저장소
SecurityContext 실제 Authentication 을 담고 있는 컨테이너
Authentication Principal, Credentials, Authorities, Details 포함
ThreadLocal 전략 요청 단위 분리, 필요시 상속 모드로 변경 가능