안녕하세요. 오늘은 스프링 시큐리티를 활용하면서 궁금했던 부분을 공부해보았습니다.
스프링 시큐리티는 SecurityContext에 인증된 Authentication 객체를 넣어두고 현재 스레드 내에서 공유되도록 관리하고 있는데요.
아래는 SecurityContext 인터페이스에 기재된 주석의 일부를 발췌했습니다.
(SecurirtContext : Interface defining the minimum security information associated with the current thread of execution.)
인증이후 편의적으로 현재 인증된 세션유저를 가져오기 위해 @AuthenticationPrincipal 어노테이션을 통해 UserDetails 인터페이스를 구현한 유저 객체를
주입할 때 사용하는 편입니다.
너무 편하게 사용했던지라 어떻게 동작하는 지에 대해서 직접 스프링 시큐리티 소스를 까보고 디버깅을 통해 공부해보고 싶었습니다.
이번 포스팅에서 이러한 내용을 공유하고자 합니다.
우선 해당 내용을 쉽게 보기 위해서는 Spring Security에 대한 선수 지식이 있다는 전제하에 기본적인 내용은 설명없이 넘어가고자 합니다.
스프링 시큐리티에 대한 선수지식은 해당 포스팅의 내용을 참조하시어 이번에 스프링 시큐리티에 대한 공부도 함께 해보시는 것을 추천드립니다.
우선 알고가야하는 클래스와 인터페이스가 존재합니다.
SecurityContextHolder : 클래스. 특정 전략에 따라 Security Context 영역을 관리하는 인터페이스입니다. 해당 클래스가 관리하는 SecurityContext 객체는 getter로 접근이 가능하다.(getContext() 메소드) 이는 현재 스레드 기준의SecurityContext를 반환한다.
SecurityContext : 인터페이스. Security Context 인터페이스. Authentication에 대한 접근 getter를 정의해놓음.
SecurityContextImpl : 클래스. SecurityContext 인터페이스를 구현한 객체. Authentication 객체에 대한 getter/setter를 정의해 놓은 객체. 해당 구현체를 통해 내부적으로 현재 스레드의(아마 1번의 request 요청일 것으로 생각되지만..) Security Context 를 생성하여 인증 후 Authentication 객체를 넣어놓는 역할을 한다.
Authentication : 인증 정보에 대한 부분을 정의해놓은 인터페이스. Principal과 Credentials, Authorities 에 대한 정의가 되어있다. 여러 구현체가 있지만 제가 썻던 스프링 시큐리티 내용에서는 UsernamePasswordAuthenticationToken 클래스를 활용했었습니다.
- principal의 의미는 "인증되는 주체의 ID" 이고,
- Credentials 은 "주체가 정확한지 증명하는 것",
- Authorities는 아시다시피 "권한"의 내용입니다.
UserDetails : 사용자 정보(username, password 등)을 가지는 인터페이스. 이를 구현하여 실제 로그인에 사용할 클래스를 만들면 된다. 스프링 시큐리티 내부적으로 직접 사용하지는 않고 Authentication 으로 캡슐화하여 저장된다. 따라서 UserDetails 구현체의 정보는 Spring Security Context에 저장된 Authentication 객체가 가져간다고 볼 수 있다.
HandlerMethodArgumentResolver : 인터페이스. 특정 전략에 따라 한 request에서 넘어온 인자들을 메소드 파라미터로 해석할 수 있도록 도와줌.
AuthenticationPrincipalArgumentResolver : 스프링 시큐리티에서 HandlerMethodArgumentResolver 를 구현한 구현체로 @AuthenticationPrincipal 어노테이션이 실제로 사용되는 부분.
실제 스프링 시큐리티 내부 코드를 보자!!
사실 클래스 설명에서 거의 대부분의 내용을 말한 것 같은데. 대부분 주석에 기반한 내용이다. 따라서 직접 내부 코드를 열어보고 확인하는 시간을 가져보려고
한다.
현재 스레드가 세션을 물고있고 있다면 이미 SecurityContextHolder의 getContext() 메소드를 통해 SecurityContext 객체를 얻고 그 안의 getAuthentication() 메소드를 통해 Authentication (인증객체)를 얻게 될 것입니다.
그렇다면 해당 컨트롤러 메소드의 @AuthenticationPrincipal 어노테이션은 실제 어떻게 동작하고 있을까??
@PostMapping(value = "/logout",
consumes = MediaTypes.HAL_JSON_VALUE,
produces = MediaTypes.HAL_JSON_VALUE)
public Object logout(@AuthenticationPrincipal SecurityUser securityUser,
@RequestHeader("Authorization") String authorization) throws Exception {
// 컨트롤러 로직
}
일단 메소드의 파라미터 영역에 Request 파라미터로 넘어온 인자를 바인딩하는 부분이 가장 의심스러워서 method argument resolve 역할을 하는 인터페이스를 구현한 부분이 Spring Security 클래스에 존재하는지를 확인해보았다.
결론적으로 말하면 우선 Spring Security를 활용하는 경우,
AuthenticationPrincipalArgumentResolver 클래스를 활용하여 resolveArgument 메소드를 구현하고 SecurityContext에 저장된 인증객체를 기반으로 Authentication 객체를 꺼내오게 된다. 아래는 실제 구현 메소드 부분이다.
컨트롤러의 파라미터마다 해당 resolveArgument 메소드가 실행되고 다음 1~ 3의 과정을 거쳐 파라미터에 주입해주게된다.
이는 현재 스레드에 Security Context에 저장된 Authentication 객체가 존재하는지에 따라 실행되므로 존재하지 않는다면 구현 메소드 내용과 같이 해당부분은 null로 바인딩 될것이다.
해당부분의 return 문이 도달하는 데에는 Security Context 에 인증정보가 저장되어있다는 전제하이고, 이는 곧 사용자는 로그인하였다는 전제하이다. 또한 개발자가 @AuthenticationPrincipal 어노테이션을 활용하여 인증주체의 ID를 주입한다는 개발내용이 있었다는 것이다.
@AuthenticationPrincipal 어노테이션을 통해 어떻게 인증정보(세션정보)를 가진 객체가 바인딩되는지를 살펴본 부분이었는데 사실 이 부분만 본다고해서 전체적인 그림을 그리기는 어렵다.
예를 들어, 세션을 사용하는 방식과 jwt 같은 토큰을 사용하는 방식에 따라서 시큐리티 설정과 처리방식이 매우 다양한 로직을 띄는데..
간단히 살펴보면 세션을 사용한다면 브라우저의 JSESSIONID를 활용하여 사용자 세션정보를 구분할 수 있으므로 특별히 요청 전에 세션정보만 잘 가져온다면 추가적인 처리가 덜하겠지만 JWT 같은 토큰을 활용한다면 요청된 요청마다의 토큰 정보(곧 세션정보가 될)를 읽어 매번 인증을 진행할 것이다. 따라서 매 요청마다 Filter를 활용하여 SecurityContext에 요청마다 인증되는 Authentication 객체를 set 시킬 것이고 이후에 Controller 에서 @AuthenticationPrincipal을 활용하여 가져올 수 있는 부분이다.
또한 principal 을 인증주체의 ID라고 정의하였으니 인증객체의 principal에 로그인 ID를 저장하고 @AuthenticationPrincipal로 Principal을 불러올 때, String loginId와 같이 유니크한 ID 값을 주입받아 해당 아이디를 조회하여 처리하는 부분이 있을 것이고,
비슷하게 principal의 인증주체의 id(혹은 데이터베이스 pk)를 hash 혹은 equals의 key로 설정하여 Unique 특성이 보장된다는 전제하에 principal에 UserDetails 구현 객체(사용자 인증정보)를 담아 직접 객체를 주입하여 처리할 수도 있는 부분이기 때문에 매우 다양하게 사용된다.
단편적으로 @AuthenticationPrincipal 어노테이션이 동작하는 부분만 보려고 하였으나 결과적으로 모든 부분을 뜯어보게 되었다.
Spring Security를 뜯어보는 것은 거의 처음인데 많은 공부가 되었다. @AuthenticationPrincipal 어노테이션이 어떤식으로 동작하는지는 사실 어느정도 감이 잡히긴 했던 부분이라 정확히 확인하는 수준이었지만 좀더 안단에서 어떻게 인증정보가 저장되고 스레드와는 어떤 부분이 있는지 왜 이렇게 많은 인터페이스와 구현체들의 조합으로 개발된건지 느낌을 받을 수 있었다.
실제 많이 사용되는 스프링 프레임워크의 어노테이션이 어떻게 동작하고 있는지 앞으로 많은 부분을 뜯어보게 될 것같다.