Spring/Spring Security

Spring Security(스프링 시큐리티)의 동작 원리(Filter, FilterChain, Filter의 구조등)

윤밥밥 2024. 6. 21. 14:03

Spring Security

이번 프로젝트에 스프링 시큐리티를 맡게 되었고

분명 작년에 해봤으니까 잘 할 수 있겠지 생각했다.

하지만 이번에는 userName(아이디)과 creatential(비밀번호)외에도 추가로 roomId를 user를 식별하기 위해 사용하면서 난항을 겪게 되었다..

내가 생각보다 시큐리티 원리에 대해 이해를 못하고 있어, 전체적인 커스텀을 못 했기 때문이다.

그래서 이번 기회를 통해 시큐리티 원리에 대해 더욱 정확히 알아보고자 한다.

스프링 시큐리티란?

스프링 시큐리티란 인증, 인가와 공격자에 대한 보호를 제공해주는 프레임워크이다. 보안과 관련된 많은 옵션을 제공해주기에, 개발자가 일일히 보안 로직을 작성하지 않고 가져다 사용하면 된다.

  • 인증(Authentication) : 사용자가 본인인지 확인하는 절차. 즉 서버 DB에 존재하는 사람인지 확인하는 절차라고 생각하면 된다.
  • 인가(Authorization) : 인증을 하더라도 각 서버의 리소스는 회장, 임원, 대리등만 볼 수 있는 권한이 있을 수 있다. 이러한 권한이 있는 지 확인하여 허용된 리소스만 제공하는 절차를 인가라고 생각하면 된다.
  • 인증, 인가를 시도하는 사람(Principal) : 서버에 접근하는 대상, 각 User를 식별하기 위한 ID라고 보면 된다.
  • 비밀번호(Credential) : Pricipal의 비밀번호이다. 앞으로 나오게 되는 인증 주체는 User가 되며 이 유저의 비밀번호를 시큐리티에서는 Credential로 부른다고 생각하고 넘어가자.

 

 

전체적인 구조

요청 → 요청을 시큐리티 단에서 가로챔 → 여러 필터를 거침 → 인증&인가 후 서블릿에 요청 전달 → 서블릿이 알맞는 컨트롤러에 요청 전달 → 서비스 단에서 요청 응답 후 응답

  1. 사용자가 서버로 요청을 한다.
  2. 시큐리티에서 요청을 서블릿으로 보내기 에 가로채서 인증 인가를 한다.
  3. 인증 인가를 하는 과정은 로그인, jwt 확인, 세션 확인, 에러 처리등 여러 각 역할에 맞는 필터(Filter)를 거치면서 각 필터에서 인증 혹은 인가를 하게 된다.
    1. 만약 특정 필터에서 인증 인가를 했다면 다음 필터로 요청을 건넨다.
    2. 이렇게 모든 필터를 거치게 된다면 최종 필터에서 서블릿으로 요청을 건넨다.
    3. 만약 필터 중간에서 인증, 인가에 실패했다면 다음 필터로 넘기지 않고 에러를 응답한다.
  4. 인증, 인가에 성공하면 서블릿으로 요청을 건넨다.
  5. 서블릿은 알맞은 컨트롤러(핸들러)에 요청을 넘겨 응답을 하게 한다.

여기서 중요한 점은 모든 필터를 거치면서 인증, 인가에 성공해야 서블릿에 요청을 보낼 수 있다는 점입니다.

다만, 로그인 필터, jwt 필터, 에러 필터등 특정 url 요청에 대해서만 필요한 필터들이 존재할 수 있습니다.

이를 위해 시큐리티에서는

  • 특정 URL에 대해서는 모든 필터를 통과하게 하기
  • 특정 필터는 특정 URL만 이 필터를 거치게 하기
  • 특정 필터는 특정 URL에 대해 이 필터를 거치지 않게 하기

등의 설정이 가능합니다.

따라서 필터나 설정파일을 적절하게 Custom하며 인증 인가를 조절할 수 있습니다.

 

 

 

필터 체인(Filter Chain)

시큐리디 단에서 거치게 되는 여러 필터들을 묶어서 필터 체인이라고 합니다. Security Context Persistence Filter부터 FilterSecurityIntercepter까지 모든 필터를 거치며 인증 인가를 확인합니다.

참고로 모든 필터가 인증, 인가 처리를 하지는 않습니다. 경우에 따라 발생한 에러를 처리해주는 필터도 존재할 수 있습니다.

또한 이 필터들 사이로 Filter를 커스텀해서 넣을 수도 있습니다.

주요 필터에 대한 간략한 소개 먼저 하겠습니다. 이해를 못 하셨다면 아래 각 필터에 대한 소개를 봐주시면 될 거 같습니다.

  • SecurityContextPersistenceFilter : SecurityContext를 영속화하는 데에 책임을 가진 필터입니다.
  • LogoutFilter : 로그아웃 기능을 처리해주는 필터입니다.
  • UsernamePasswordAuthenticationFilter : 기본적인 유저 인증을 처리해주는 필터입니다.

1. SecurityContextPersistenceFilter

SecurityContext를 영속화하는 데에 책임을 가진 필터입니다.

그럼 SecurityContext는 무엇일까요?

SecuriryContext란 사용자 인증 및 권한 정보를 담고 있는 Authentication을 저장하는 보관소이다.

 

세션 방식 or jwt 방식에 따라 설명에 조금 차이가 있을 거 같습니다.

 

세션 방식

세션 방식이란 사용자 인증 정보를 담은 세션을 만들고 각 세션 별로 세션 id를 만들어냅니다.

그리고 세션 id는 클라이언트가 쿠키에 보관하고

세션은 서버에 저장해둡니다.

 

요청이 올 때마다 요청에 포함된 세션 id를 확인하여 인증을 합니다.

 

이 세션 방식에서는 세션이 Authenication이고 세션을 보관하는 보관소가 SecurityContext라고 생각하시면 됩니다.

 

또한 이러한 보관소들을 필요할 때마다 꺼내쓰는 주체가 SecurityContextHolder라고 생각하시면 될 거 같습니다.

 

JWT방식

jwt방식은 세션의 문제점을 보완하고자 나온 방식입니다.

서버에 세션을 매번 저장하고 있는 것은 서버의 과부하를 일으킬 수 있기 때문에

이러한 정보를 클라이언트가 갖게 하는 방식입니다.

 

즉 사용자 정보를 클라이언트가 가지고 있고, 매 요청 시마다 이 jwt토큰을 요청에 포함하여

서버가 인증하는 방식입니다.

 

따라서 jwt 방식에서는 사용자 인증 정보를 요청에 보내기 때문에 세션을 저장할 필요가 없습니다.

그럼에도 불구하고 jwt방식에서도 SecurityContext를 사용합니다.

왜일까요?

 

SucuriryContext의 추가 기능

시큐리티 단과 로직을 처리해주는 서비스단(서블릿)은 분리되어 있습니다.

따라서 사용자 인증 정보 같은 경우에는 서비스단에서 쉽게 사용하기 힘듭니다.

더군다나 세션 방식에서는 요청에 사용자 정보가 없기 때문에 서블릿에서는 사용자 정보를 더욱 사용하기 힘듭니다.

 

따라서 매 요청시마다 사용자 정보를 Authentication이라는 곳에 저장하고, 이를 SecurityContext에 담고 영속화합니다. (세션 방식에서는 이미 저장되어 있을 때 꺼내옵니다.)

 

이렇게 영속화된 정보는, 서버의 특정 메모리에 저장되어 있기 때문에 서비스단에서도 접근할 수 있게 됩니다.

 

이러한 장점 때문에 사용자 정보를 쉽게 가져오기 위해 jwt 방식에서도 SecurityContext를 사용합니다.

 

추가적으로 이러한 영속화된 SecurityContext는 매 요청이 끝날 때마다 비영속화 해줍니다.

세션 방식에서는 Repository에 저장해두었기에 나중에 다시 메모리에 올려 사용할 수도 있고

jwt 방식은 Repository에 저장하지 않기 때문에, 완전히 삭제됩니다.

 

하여튼 이렇게 SecurityContext를 영속화해주는 주체가 SecurityContextFilter며,

매 요청이 끝날 때 다시 이 필터로 되돌아오면, Context를 비영속화 해줍니다.

 

 

2. UsernamePasswordAuthenticationFilter

Spring Security에서 제공하는 필터 중 하나로, 사용자 이름(username)과 비밀번호(password)를 이용하여 인증을 처리하는 역할을 합니다.

즉 UsernamePasswordAuthenticationFilter 는 DB에서 사용자 정보를 가져와,

요청한 사용자 인증 정보와 비교해 인증을 처리합니다.

여기서

DB에서 가져온 User 정보를 담는 UserDetails

DB에서 User 정보를 가져오는 UserDetailsService

가져온 User정보와 사용자 인증 정보를 비교해 인증을 처리하는 AuthenticationProvider

각 요청에 알맞은 AuthenticationProvider를 제공해주는 AuthenticationManager

등이 존재합니다.

이는 아래에서 다시 다루겠습니다.

 

 

3. FilterSecurityInterceptor

인증된 사용자의 권한을 검사하는 필터로, URL 기반의 접근 제어를 수행합니다. 즉 요청에 대한 인가 검사를 진행합니다.

4. ExceptionTranslationFilter

예외 처리를 담당하는 필터로, 인증 및 인가 관련 예외를 적절히 처리하여 HTTP 응답 코드를 설정하거나, 인증 실패 후의 리다이렉트 처리 등을 합니다.

5. 기타

아래 명시된

  • SecurityContextFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • DefaultLoginPageGeneratingFilter(기본 로그인 페이지를 생성하는 필터)
  • DefaultLogoutPageGeneratingFilter(기본 로그아웃 페이지를 생성하는 필터)

7가지 필터는 기본으로 등록되는 필터입니다.

이 외의

  • CocurrentSessionFilter
  • RememberMeFilter

등은 개발자가 직접 등록해주어야 하는 필터입니다.

 

저는 참고로 여기서 id, pw 외의 추가 정보를 사용자 인증 정보로 사용하였기에

UserNamePasswordFilter를 커스텀해서 등록해주었고(따로 등록을 해주면 등록된 필터로 대체됩니다.)

jwt를 사용했기에 jwtFilter를 만들어 새로 추가해주었습니다.

 

여러분들도 각 필터의 기능을 파악하여, 존재하는 필터는 SpringSecurity에서 가져와 사용하시고

없으면 새로 추가하여 필터체인에 넣어주시면 될 거 같습니다.

 

 

각 필터의 동작원리

요청이 들어오면, 각 필터를 거치게 되며 필터는 다음과 같은 과정을 가집니다.

(참고로 모든 필터가 다음과 같은 필터 구조를 띄는 것은 아닙니다. 주로 로그인 관련 필터가 다음과 같은 구조를 띈다고 생각하면 될 거 같습니다.)

 

  1. 요청이 들어오면 이를 시큐리티단에서 가로챈다.
  2. 필터체인의 필터들이 인증 인가를 한다.
  3. 특정 필터에 요청이 들어온다.
  4. 그럼 사용자의 정보, 인가 정보, 권한 정보를 담기 위한 UsernamePassworAuthenticationToken을 생성한다.(참고로 이 토큰이 SecurityContext에 담길 토큰 객체이다. 또한 UsernamePassworAuthenticationToken은 인증된 객체, 인증되지 않은 객체를 구분하여 생성할 수 있기에 비인증 객체에는 요청에서 처리할 사용자 정보만 담는다.)
  5. 이 Token을 AuthenticationManager에 건넨다.
  6. Token을 받은 AuthenticationManager는 개발자가 Manager에 등록한 AuthenticationProvider에 인증을 요청한다.(즉 AuthenticationManager은 Provider를 관리하고, 인증을 요청하는 관리자 역할)
  7. 인증을 요청받은 AuthenticationProvider는 UsernamePassworAuthenticationToken에 존재하는 사용자 정보(유저 ID등)을 이용하여, UserService에게 요청하여 해당하는 User를 가져온다.
  8. 사용자 정보를 받은 UserDetailsService는 DB에서 User 정보를 가져오고 이를 UserDetails라는 객체에 담아 AuthenticationProvider에 리턴한다.
  9. UserDetails를 리턴받은 AuthenticationProvider는 Token 정보와 UserDetails정보를 비교하여 같은 지 확인한다.
    1. 만약 정보가 일치한다면 새롭게 UsernamePassworAuthenticationToken를 만들어 사용자 정보에 더불어 인증 정보, 사용자 권한 정보를 넣어 리턴한다.
    2. 만약 일치하지 않는다면 예외를 던진다.(이는 ExceptionTranslationFilter가 받아 예외 응답을 리턴한다.)
  10. Token을 리턴 받은 AuthenticationManager는 다시 Filter에 Token을 리턴한다.
  11. Token을 리턴 받은 Filter는 이 Token을 영속화된( SecurityContextPersistenceFilter가 가장 먼저 요청을 받아 SecurityContext를 영속화하고 다음 필터에 넘겼기에 이미 SecurityContext는 영속화 되어있다.) SecurityContext에 AuthenticationToken(Token)을 넣는다.
  12. 다음 필터로 이동한다.

참고로 일반적인 UserName과 Password를 통해서 인증을 하려면 SpringSecurity가 제공하는 DaoAuthenticationFilter를 사용하시면 될 거 같습니다.

 

다만 저는 새로운 RoomId도 포함하여 유저를 구분했기 때문에

UserDetails, UserDetailsService, AuthenticationToken, AuthenticationProvider를 모두 커스텀했습니다.

 

이렇게 스프링 시큐리티에 대한 전반적인 구조를 알아봤습니다.

여러분들도 완벽히 원리를 이해하셔서 좋은 Securiry를 구현하셨으면 좋겠습니다!

 

'Spring > Spring Security' 카테고리의 다른 글

Spring MVC와 Security에서의 CORS 설정  (1) 2024.08.16