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

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

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


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

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

이를 알기 위해서는 SecurityContextSecurityContextHolder의 구조가 어떤지 알고,
이게 스프링 시큐리티 내부의 어느 필터에서 사용되는지를 알아야 한다.
필터는 다음에 알아보고 먼저 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개의 댓글