[Spring Security] 아키텍처 이해 - 인증 저장소( SecurityContextHolder, SecurityContext )

식빵·2022년 8월 16일
post-thumbnail

이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.


우리가 인증을 수행하고 나면 스프링 시큐리티 내에서는 인증 결과(=인증토큰)를
SecurityContext 라는 곳에 저장하여, 전역적으로 사용이 가능하도록 한다.

그렇다면 대체 SecurityContext는 무엇이며, 어떻게 전역적으로 사용이 가능한걸까?

이를 알기 위해서는 SecurityContext 및 SecurityContextHolder의 구조가 어떤지 알고,
이게 스프링 시큐리티 내부의 어느 필터에서 사용되는지를 알아야 한다.
필터는 다음에 알아보고 먼저 SecurityContext, SecurityContextHolder클래스에 대해 알아보자.




🥝 SecurityContext, SecurityContextHolder


1. SecurityContext

  • Authentication 객체가 저장되는 보관소
  • SecurityContextHolder 전략(Strategy)에 따라 SecurityContext의 저장 방식이 다름
  • 하지만 일반적으로는 ThreadLocal 에 저장
  • 덕분에 코드 어디서나 Authentication 을 꺼내서 사용가능
  • 추가적으로 인증이 완료되면 세션에도 저장됨

참고: ThreadLocal?
쓰레드마다 갖는 고유한 저장공간이라고 생각하면 된다.


2. SecurityContextHolder

  • SecurityContext 를 감싸는(저장하는) 객체
  • (일반적으로) SecurityContext 저장을 위한 ThreadLocal 를 갖고 있는 객체
  • SecurityContext 객체의 저장 방식(전략, Strategy)을 지정
    • MODE_THREADLOCAL : 스레드당 SecurityContext 객체를 할당, 기본값
    • MODE_INHERITABLETHREADLOCAL: 메인, 자식 스레드에서 동일한 SecurityContext 사용
    • MODE_GLOBAL: 프래그램에서 딱 하나의 SecurityContext만 저장
  • 각 전략에 따른 전략 클래스가 존재하며, SecurityContextHolder의 메소드 대부분이 이 전략 클래스의 인스턴스에게 작업을 위임하는 형태로 동작한다.
  • SecurityContextHolder.clearContext() : 기존 SecurityContext 정보 초기화

참고: SecurityContextHolder, SecurityContext 를 통한 인증객체 읽는 법

Authentication auth 
	= SecurityContextHolder.getContext().getAuthentication();





🥝 인증 프로세스와 저장소


  • 로그인 시도
  • 요청을 받는 스레드 하나 생성
  • 해당 스레드의 고유한 저장소인 ThreadLocal이 존재함
  • 인증을 실패하면 SecurityContextHolder.clearContext()
  • 인증에 성공하면 SecurityContextHolder > SecurityContext 에 인증 토큰 저장
  • HttpSession 에도 SecurityContext 저장





🥝 코드 관찰하기


위에서 설명한 것들을 코드로 직접 보고 이해해 보자.

1. SecurityContextHolder

SecurityContextHolder 의 java document 를 읽어보면 앞에 이런 문구가 나온다.

Associates a given SecurityContext with the current execution thread.
This class provides a series of static methods that delegate to an instance of SecurityContextHolderStrategy. (생략)

"이 클래스(SecurityContextHolder)는 다양한 static 메소드를 제공하고, 해당 메소드들은 내부적으로 SecurityContextHolderStrategy에게 위임처리를 한다"

즉 SecurityContextHolder 의 실제 일처리는 SecurityContextHolderStrategy 가
한다는 것을 알 수 있다.
그래서 SecurityContextHolder 초기화 시, initializeStrategy 메소드(위 그림 참고)가 호출되고, 내부적으로 SecurityContextHolderStrategy 를 결정하게 된다.

이때 어떤 모드로 세팅했는지에 따라 달라지는데, 기본값은 MODE_THREADLOCAL 모드이기 때문에
아무 설정을 안하면 ThreadLocalSecurityContextHolderStrategy 를 사용한다.
이름 그대로 쓰레드 로컬을 사용해서 SecurityContext 를 보관하는 전략이다.



2. ThreadLocalSecurityContextHolderStrategy

MODE_THREADLOCAL 는 스레드당 하나의 SecurityContext 객체를 할당하는 전략이라 언급했다.
그리고 MODE_THREADLOCAL을 위한 SecurityContextHolder 의 내부 전략(Strategy)가
바로 ThreadLocalSecurityContextHolderStrategy이다.

SecurityContext 객체를 스레드당 하나만 생성한다는 전략을 위해서 이 전략 클래스는
ThreadLocal<SecurityContext> 필드를 하나 갖고 있다.


그렇다면 이 ThreadLocal 에 저장하는 SecurityContext 에 대해서 알아보자.
밑에 있는 createEmptyContext()에서 new SecurityContextImpl()을 통해서
SecurityContext를 생성하는 것을 확인할 수 있다.



3. SecurityContextImpl

여기서는 Authentication 과 관련된 setter, getter 가 있다는 것만 알고 넘어가자.


참고: SecurityContextHolder, SecurityContext 사용법

// SecurityContext 저장 ( 직접 쓸 일이 많지 않음 )
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
emptyContext.setAuthentication(authentication1);
SecurityContextHolder.setContext(emptyContext);

// SecurityContext 조회 ( 가장 많이 쓰는 방식 )
SecurityContextHolder.getContext();

// SecurityContext 초기화 ( 직접 쓸 일이 많지 않음 )
SecurityContextHolder.clearContext();





🥝 SecurityContext 조회하기


1. RestController 클래스 작성

@RestController
public class SecurityController {
    
    @GetMapping("/")
    public String index(HttpSession session) {
        
        Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
        Authentication authentication2 = null;
        
        if (session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)
            instanceof SecurityContext securityContext) {   // java 17 문법입니다!
            authentication2 = securityContext.getAuthentication();
        }
    
        System.out.println("authentication1 = " + authentication1);
        System.out.println("authentication2 = " + authentication2);
    
        System.out.println("authentication1 hashCode = " + authentication1.hashCode());
        System.out.println("authentication2 hashCode = " + authentication2.hashCode());
        return "home";
    }
}



2. Spring Security Config 클래스 작성

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/favicon.ico");
        web.ignoring().mvcMatchers("/error");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin();
    }
    
}



3. 서버 실행 및 로그 확인

  1. 서버를 실행
  2. "/" 경로로 접속 시도
  3. 로그인창 뜸, 로그인 시도

이렇게 하고 콘솔창을 확인해보자.

확인해야 될 점은 크게 2가지다.

  1. SecurityContextHolder, HttpSession 모두 SecurityContext 를 들고 있다.
  2. SecurityContext 를 얻어온 방식은 달라도 동일한 SecurityContext 인스턴스다.
profile
백엔드 개발자로 일하고 있는 식빵(🍞)입니다.

0개의 댓글