개발놀이터

스프링 시큐리티 동작 원리 본문

Spring/Spring Security

스프링 시큐리티 동작 원리

마늘냄새폴폴 2023. 5. 17. 14:47

이번에 JWT 토큰 인증 방식에 대해서 공부하고 있습니다. 제 프로젝트에 인증 부분이 세션 + Redis + Spring Security 이렇게 세가지 부분으로 돌아가는데 여기에 JWT라는 새로운 레이어가 들어가면 보안적으로 어떨지 궁금했습니다. 

 

알아보던 중에 정말 괜찮은 블로그를 발견했습니다. 

 

https://wildeveloperetrain.tistory.com/57

 

spring security + JWT 로그인 기능 파헤치기 - 1

로그인 기능은 거의 대부분의 애플리케이션에서 기본적으로 사용됩니다. 추가로 요즘은 웹이 아닌 모바일에서도 사용 가능하다는 장점과 Stateless 한 서버 구현을 위해 JWT를 사용하는 경우를 많

wildeveloperetrain.tistory.com

이분 블로그에 있는 코드 퀄리티가 정말 혀를 내두를정도로 좋아서 저도 나중에 보고 따라해볼 생각입니다. 블로그를 먼저 보시고 맨 마지막에 깃헙 코드를 보고 따라 하시면 될 것 같습니다. 

 

위 포스팅의 주된 목적은 포스팅 제목에도 있지만 

 

"스프링 시큐리티와 JWT를 혼합할 수는 없을까?"

 

그냥 단순히 JWT토큰 발급하고 필터 중간에 끼워넣는 수준이 아닙니다. 위 포스팅 저자는 스프링 시큐리티에대한 깊은 이해를 가지고 있는 것 같았는데요. 

 

스프링 시큐리티에서 전역적으로 관리하는 Authentication 객체를 뽑아내서 JWT 토큰으로 만들고 Redis에 넣어버리는 보안적으로 봤을 때 실로 놀라울 정도로 안전할 것 같은 방법을 사용합니다. 

 

그리고 Access Token 과 Refresh Token을 사용해서 사용자를 계속 로그인할 수 있게 했습니다. (물론 서버가 내려갔다가 올라가면 어떻게 될진 모릅니다.)

 

위와 같은 방법에 크게 감명받았습니다. 그래서 저도 스프링 시큐리티에대한 깊은 이해가 필요하다고 생각했습니다. 

 

Spring Secutiry 동작 원리

스프링 시큐리티는 8개의 단계로 작동합니다. 

 

  1. FilterChainProxy에 의해 Request를 인터셉터 : 클라이언트가 스프링 애플리케이션으로 요청을 보내면 요청은 FilterChainProxy라는 일련의 필터에 의해 인터셉터 됩니다. 이 필터들의 체인은 스프링 시큐리티가 제공하는 Security 기능들을 벌크연산하는 역할을 수행합니다. 
  2. Security Filter 작동 : FilterChainProxy는 특정한 security task를 각각 수행하는 몇몇 필터들의 요청을 위임합니다. 이 필터들은 인증, 인가, CSRF 보호, session management와 같은 일들을 수행합니다. 
  3. Authentication (인증) : AuthenticationFilter인 UsernamePasswordAuthenticationFilter (POST요청의 form 데이터를 거르는 필터, OAuth 2.0같은 경우는 OAuth2ClientAuthenticationProcessingFilter가 작동합니다.) 가 Authentication객체를 생성합니다. 이때의 Authentication객체는 아직 인증된 객체는 아닙니다. 그리고 이 객체를 AuthenticationManager객체 (실질적인 객체는 ProviderManager) 에 전달하고 AuthenticationProvider가 인증을 마칩니다. 
  4. SecurityContext Updated : Authentication 객체를 인증하고 SecurityContext에 담고 이 객체는 다시 SecurityContextHolder에 담아집니다. 
  5. Authorization (인가) : 인증이 끝난 후에 인증된 유저에게 특별한 권한을 부여합니다. 그리고 이 후 AccessDecisionVoter를 이용해 인증된 Authentication객체를 적절히 인가된 접근인지 확인합니다. 
  6. Exception Handling : 만약 인증이나 인가에서 실패한다면 예외가 발생하게 되고 이 예외는 다양한 Exception Handler에 의해 관리됩니다. 
  7. Request 이행 : 만약 유저가 성공적으로 인증과 인가를 마쳤다면, 애플리케이션의 컨트롤러 메서드들에 도달할 때까지 필터 체인을 통해 요청이 진행됩니다. 요청이 진행된 후 응답은 필터체인을 거꾸로 타고 올라와서 추가적인 프로세싱을 진행합니다. 
  8. Session 관리 : 스프링 시큐리티는 전반적인 세션 관리 지원을 포함하고 있습니다. 이 지원은 session fixation을 막을 수 있고 동시에 세션을 관리할 수 있으며 세션 만료를 관리합니다. 

 

글로만 보면 정말 복잡합니다... 저도 이 과정을 이해하는데 굉장히 애먹었는데요. 이해하기 힘드시면 선형적으로 알고계시면 됩니다. 

 

  1. FilterChainProxy 작동
  2. Security Filter 작동
  3. Authentication (인증)
  4. SecurityContext 업데이트
  5. Authorization (인가)
  6. Exception Handling
  7. Request 이행
  8. Session 관리

 

인증

 

이제 스프링 시큐리티의 중요한 부분 중 하나인 인증에 대해서 자세히 알아보도록 하겠습니다. 

 

인증을 도식화하면 다음과 같습니다. 

 

 

이 도식화는 이어서 설명할 내용과 계속 번갈아서 보시면 이해하기 편합니다. 

 

순서와 함께 한번 알아보죠

 

1.  UsernamePasswordAuthenticationFilter에서 request를 가로챈다. 

UsernamePasswordAuthenticationFilter에 들어온 모습입니다. 우리가 가장 먼저 만나는 메서드는 바로 attemptAuthentication입니다. 

 

이 메서드는 request를 중간에 가로채서 form 안에 있는 username 과 password를 가지고 UsernamePasswordAuthenticationToken을 만들고 Authentication객체인 AuthenticationManager를 호출합니다. 

 

2. AuthenticationManager의 디폴트 구현체인 ProviderManager에게 Token을 위임

AuthenticationManager는 디폴트 구현체인 ProviderManager에서 UsernamePasswordAuthenticationToken을 처리할 수 있는 AuthenticationProvider를 Provider중에 찾습니다.

 

위에서 네모친 부분이 바로 Provider를 순회하면서 알맞은 AuthenticationProvider를 찾는 과정입니다. 

 

3. AuthenticationProvider를 구현한 구현체를 Provider로 설정하고 UserDetialService를 가지고 Principal을 만든다

AuthenticationProvider를 구현한 구현체중 디폴트 값인 AbstractUserDetailsAuthenticationProvider안에 있는 코드입니다. 

이 retrieveUser라는 곳이 핵심인데요. retrieveUser를 타고 들어가면 DaoAuthenticationProvider라는 구현체로 들어오게 됩니다. 

 

이 안에서 

 

UserDetailService를 가져오게 됩니다. 한번이라도 시큐리티를 사용해보신 분들은 다들 익숙하실 그 메서드! 

 

바로 loadUserByUsername을 구현하는 클래스가 바로 UserDetailService을 구현한 커스텀 클래스입니다. 

 

제가 현제 개발중인 프로젝트의 한 부분입니다. 이렇게 loadUserByUsername을 구현해줘야 그 안에 가지고 있는 User객체를 Principal로 만들어서 전달해줄 수 있는 것입니다. 

 

4. 우리가 만든 Principal을 가지고 Authentication 객체를 만든다.

Principal을 만들었다면 이제 createSuccessAuthentication으로 들어오게 됩니다. 이곳에서 Principal과 credential, 인가값을 가지고 새로운 토큰을 만들어서 Authentication객체를 만듭니다. 

 

그러면 SecurityContext에 담기게 됩니다. 

 

이곳은 SecurityContextHolder 내부입니다. SecurityContextHolder에는 총 세가지 설정이 있는데요. 

 

1. MODE_THREADLOCAL

2. MODE_INHERITABLETHREADLOCAL

3. MODE_GLOBAL

 

이렇게 세가지입니다. 우리는 기본 설정인 MODE_THREADLOCAL로 타고들어가게 됩니다. 

 

그리고 저 ThreadLocalSecurityContextHolderStrategy() 로 객체가 만들어지면?

 

이렇게 ThreadLocal이 기다리고 있습니다. 스레드로컬에 대해서 궁금하신 분들은 아래의 링크를 확인해주세요!

 

https://coding-review.tistory.com/92

 

동시성문제와 스레드 로컬

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 고급 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요 이번 시간에는 스프링을 사용할 때 주의할 점과 해결방법인 스레

coding-review.tistory.com

 

스레드 로컬로 Authentication객체가 관리되기 때문에 전역적으로 어디서든지 사용해도 멀티스레드에의한 동시성 문제가 발생하지 않습니다. 

 

이는 스레드로컬이 가지고있는 특징중 하나인데요. 간단하게 설명하자면 스레드로컬은 내부적으로 Map을 사용합니다. 

 

Map을 사용하기 때문에 Key만 같다면 같은 값을 반환받는 것을 보장하죠. 

 

하지만 스레드로컬 저장소는 한번의 요청이 올 때 동안만 굉장히 유용합니다. 왜냐하면 각각 다른 스레드가 각기다른 요청을 멀티스레드 환경에서 처리하기 때문입니다. 

 

즉, 이말은 서버가 한번 내려가면 혹은 사이트를 벗어나면 세션이 풀리게 된다는 말입니다. 

 

이를 해결하기 위한 방법으로 시큐리티에서 지원하는 RememberMe도 조만간 포스팅해보도록 하겠습니다. 

 

 

 

마치며

여기까지 스프링 시큐리티에대한 전반적인 동작과정 그리고 인증에 대한 심층적인 이해까지 다뤄봤습니다. 직접 스프링을 타고타고 들어가는 경험은 몇번을 해도 놀라울 따름입니다. 

 

어떻게 이런걸 구현했지... 싶은 경외심이 듭니다. 스프링 시큐리티를 사용하는 분들이라면 이번 기회에 스프링 시큐리티에 대한 전반적인 이해를 추천드립니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~