서론
Spring Security를 Security 5로 공부를 진행해나가면서 Security6 에서 많은 부분이 변경되었다는 얘기를 듣고 공식문서를 보면서 학습한 내용에 대해 Security5를 Secuirty6로 Migration을 진행 해 나갔습니다.
그 중에서 SecurityFilterChain에 등록을 할 때 정적 자원들에 대해서 ignoring 설정을 해주지 않으면, SecurityFilterChain에서 이전에 등록되어있는 정적 자원들에 대해 모든 Filter들이 적용이 되기에 쓸데없는 메모리 낭비가 발생하고, ignoring설정을 해서 새로운 SecurityFilterChain을 만들어 주되 Filter 들은 추가되지 않는 방향으로 성능 개선을 진행한 사례를 볼 수 있었습니다.
이에 대해, Security6 에서는 어떤 형태로 ignoring을 하는지 찾아보게 된 결과 다음과 같은 사진을 공식문서에서 볼 수 있었습니다.
사진의 내용을 요약하면 다음과 같습니다.
지금까지는 정적 자원에 대해 무시하는 형태로 설정을 했었다.
하지만, 이제는 정적 자원에 대하여 permitAll을 적용해서 정적 자원들에 대해서도 보안적인 문제를 해결하면서 성능도 개선이 되었다. 이는 spring security6 에서 매 요청마다 session을 생성했던 것에 대한 문제를 해결했기 때문이다.
도대체 Session의 어느 부분이 변경되었기에, permitAll을 하더라도 메모리 낭비가 없으며, 이전에는 어떻게 Session을 생성했기에 메모리 낭비가 발생했다는 것인지 궁금한 부분이 생겼고, 이에 대해 Session 관련된 부분을 살펴보고자 하였습니다.
Security 5의 Session 관리
위와 같은 고민을 가지고 또 다시 공식 문서에서 Session 관리가 어떤 식으로 변경이 되었는지 찾아봤고, 다음과 같은 내용을 볼 수 있었습니다.
해당 부분도 요약을 하면 다음과 같습니다.
Spring Security 5에서는 기본적인 행동이 SecurityContextRepository가 SecurityContextPersistenceFilter를 사용하였고, 이때 매 요청마다 세션을 생성하였는데 이는, 정상적인 요청과 응답이 수행되기 전에도 세션을 생성한다는 문제점과, 이러한 세션의 추적이 어렵다는 문제가 있었다.
Spring Security 6는 위와 같은 문제를 해결하기 위해 기본적인 동작은 SecurityContextHolderFilter를 사용하며 이는 SecurityContext를 SecurityContextRepository로 부터 읽기 동작만을 수행을 합니다. 또한 세션의 저장은 유저가 요청 시에 명시 할 경우에만 세션을 생성하도록 변경하였습니다.
다음과 같이 명시가 되어 있는 부분을 볼 수 있었습니다. 이제 위에서 얘기했던 문제에 대해서 어떤 문제가 있었는지 어렴풋이 짐작은 가능합니다. 클라이언트가 세션을 생성하고자 요청하지 않았음에도 불구하고, 서버 쪽에서 임의로 세션을 생성했기에 메모리 누수가 발생했다는 부분인데, 어떻게 코드가 짜여있었길래 위와 같은 문제점이 발생했었는지를 살펴볼 필요가 있었습니다.
SecurityContextPersistenceFilter 분석
SecurityContextPersistenceFilter의 doFilter() 부분입니다. 위에 if문을 통한 선행 리턴은 해당 Filter가 요청에 대해 한 번만 실행되도록 보장하기 위한 분기이고, 아래 네모 친 부분을 보면 request.getSession(); 이 보입니다.
request.getSession() 은 request.getSession(true); 와 같고 이는 다음과 같습니다. 세션이 있다면 해당 세션을 반환하고, 없다면 새로운 세션을 생성하라.
만약 session을 생성하지 않도록 하고 싶다면 request.getSession(false); 로 설정하면 될 것 입니다. 이는 session이 있을 경우에는 해당 session을 반환하고 없다면, null을 반환하기 때문에 null 체크가 필요합니다.
그럼 여기서 또 의문이 드는 부분이 생기게 됩니다. 바로 위에 조건식인 this.forceEagerSessionCreation 이란 무엇일까?
찾아가 보면 다음과 같은 내용을 볼 수 있습니다.
해당 박스 친 부분을 살펴보면 세션 생성 정책이 ALWAYS 라면 위에서 봤던 forceEagerSessionCreation을 true로 전달함으로써 자동으로 생성하도록 해주고 있습니다. 여기서 Spring Security에서 제공하는 세션 생성 전략 4가지를 살펴보겠습니다.
- ALWAYS : 스프링 시큐리티가 항상 세션을 생성한다.
- NEVER : 스프링 시큐리티가 세션을 생성하지 않지만, 기존에 존재하면 사용한다.
- IF_REQUIRED : 스프링 시큐리티가 필요하다면 생성을 한다. (Default)
- STATELESS : 스프링 시큐리티가 생성하지도 않고 기존 것을 사용하지도 않는다. -> JWT 토큰 방식을 쓸 때 사용을 한다.
기본 값이 IF_REQUIRED 이기 때문에, 세션이 자동으로 생성 될 것으로 예측되지 않습니다. ALWAYS 여야 Session을 생성한다는 것을 우리는 위에서 이미 살펴봤습니다. 하지만, IF_REQUIRED 임에도 문제가 되는 부분이 있습니다. 바로 필요하다면 세션을 생성한다는 부분입니다.
Spring Security는 기본값으로 SecurityContext를 생성하는 전략으로 HttpSessionSecurityContextRepository를 사용합니다. 결국 SecurityContext를 저장하기 위해서 HttpSession을 사용해야 하고, 이 말은 곧 스프링 시큐리티가 필요하다면 생성을 한다는 말과 부합하기때문에 Session을 생성 할 수 밖에 없던 것입니다.
해당 코드는 다음에서 살펴볼 수 있습니다.
this.repo는 SecurityContextRepository를 참조하고 있고, 이에 대한 기본 구성 클래스로 HttpSessionSecurityContextRepository를 사용합니다. 결국 Security는 이 부분에서 Session 생성 전략을 IF_REQUIRED로 설정하여, 항상 생기지 않도록 하였음에도 불구하고, 기본 값으로 Session에 SecurityContext를 저장하도록 하여 Security는 세션이 필요하다고 판단. 이에 따라 세션을 매번 생성 하는 것과 동일한 결과를 나타내는 것이었습니다.
이제 Security5 에서 Session을 어떻게 관리를 했는지, 그리고 어떠한 이유로 Session을 매번 생성하는 효과를 나타냈는지에 대해서 알게 되었습니다. 이어서 Security6의 변경 점을 살펴보도록 하겠습니다.
Spring Security 6 Session 변경 점
우선 공식문서에 적혀있듯이 Spring Security 6에서는 SecurityContextPersistenceFilter가 Deprecated 되었습니다.
그럼, 이 대신에 사용하는 SecurityContextHolderFilter는 어떻게 세션을 관리하고 있는지 어떻게 변경되었는지 알아보고자 합니다.
Spring Security 6의 SecurityContextHolderFilter의 doFilter() 부분입니다. 우선 이전의 SecurityContextPersistenceFilter 에 비해서 상당히 코드 수가 적어진 부분을 확인 할 수 있고, 중요한 부분은 박스 친 부분을 통해서 SecurityContext를 불러오는 것을 볼 수 있습니다.
여기서 이어지는 의문이 듭니다. SessionCreationPolicy에 Default에 대한 변경점은 없으며, SecurityContextRepository 또한 HttpSessionSecurityContextRepository를 기본 값으로 가져가는 부분 또한 동일합니다. 따라서 어떤 부분이 변경이 되었는지 해당 코드로 들어가볼 필요가 있습니다.
SecurityContextRepository에 default 메서드를 볼 수 있습니다. Supplier를 반환 값으로 가지고 있고, Deprecated된 loadContext를 호출하니 다시, 해당 코드로 들어가 볼 필요가 있습니다.
여전히 기본 값으로 HttpSessionSecurityContextRepository를 사용하고 있기 때문에 HttpSessionSecurityContextRepository의 loadContext 메서드를 보도록 하겠습니다.
위 사진에 있는 코드에 따라 HttpRequestResponseHolder에는 getResponse()가 null로 반환될 것이 자명하고, 3번 째 라인의 request.getSession(false); 로 인하여 HttpSession 또한 null이 반환 될 것입니다.
4번째 줄 코드를 살펴보면 readSecurityContextFromSession() 메서드를 살펴볼 수 있습니다. 해당 메서드를 살펴보면
아래에도 더 긴 코드들이 있지만 전달받은 httpSession이 null이기 때문에 null을 선행 반환 하게 될 것입니다. 반환된 SecurityContext가 null이기 때문에 아래 if문의 분기를 수행하게 됩니다. generateNewContext()를 살펴보도록 하겠습니다.
securityContextHolderStrategy 전략에 따라 EmptyContext를 반환하는 것을 볼 수 있습니다. SecurityContextHolderStrategy의 기본 구성 클래스는 ThreadLocalSecurityContextHolderStrategy 이기 때문에, 해당 클래스의 createEmptyContext()를 살펴보도록 하겠습니다.
SecurityContextImpl()을 반환하고 있는 것을 볼 수 있고, 다시 해당 코드로 추적을 해보도록 하겠습니다.
기본 생성자를 살펴보면 Authentication 필드에 대한 초기화가 없이 해당 객체를 생성하는 것을 볼 수 있습니다.
이에 따라, Authentication 또한 null을 반환하는 것이 자명합니다.
그리고 이는 ThreadLocal을 통해 해당 ThreadLocal에서 해당 SecurityContext의 getAuthentication()을 수행 하게되면 null이 반환되는 이유와도 같습니다. 여기까지 역 추적을 해오면서 그 어디서도 Session을 생성하는 부분을 살펴 볼 수 없었습니다.
이러한 부분으로 인해 Session에 대한 생성 개선이 이루어 진 것을 볼 수 있었습니다.
결론
- 세션 생성 전략의 기본 값이 IF_REQUIRED 임에도 불구하고, 저장을 하기 위해서 Session을 사용해야 했기에 결국 항상 Session을 사용해야 했다. 그리고 이러한 필터는 SecurityContextHolderFilter가 담당했다.
- 이러한 문제를 해결하기 위해 Security6 에서는 SecurityContextHolderFilter만을 사용하고 유저가 명시할 때에만 saveContext를 수행하도록 변경되었다.
- 이에 따라, Spring Security 6부터는 정적 컨텐츠에 대해 ignoring()을 하지 않더라도 크게 성능 저하가 없는 것으로 간주한다.
의문
Session을 사용하지 않아 성능이 개선된 부분, 즉 Active User가 많을 경우에 쓸데없는 메모리 낭비가 없어졌기 때문에 Spring에서는 permitAll()을 권장한다고 하는 것 같다고 판단된다.
하지만, 이전과 같이 정적 컨텐츠들에 대해서도 Filter가 적용되는 것을 볼 수 있는데 정적 컨텐츠에 대해서도 Security의 보안관련 filter들이 적용되는 것이 더 안전하다는 측면에서 이를 사용하라고 하는 것 같다. 정적 컨텐츠에 대해서 어떤 부분에 대한 Filter가 적용되어야 하고 어떤 보안상의 이슈가 존재 할 것이라고 판단하는 것일까? 이 부분이 해결되면 아래에 추가하도록 하겠습니다.
'Backend > Spring' 카테고리의 다른 글
막내 개발자의 사내 분산 락 라이브러리 도입이야기 (1) (2) | 2024.11.10 |
---|---|
Spring Security OAuth2 주요 용어와 인증 방식 (0) | 2023.08.14 |
Invalid character found in method name 에러 (2) | 2023.05.07 |
Spring AOP (0) | 2023.04.26 |
스프링의 빈 후처리기 (0) | 2023.04.24 |