본문 바로가기
Back-End/Spring Security

[Spring Security] 구성 요소 및 인증 과정

by 달의 조각 2022. 9. 22.

인증 처리 흐름

 

  • 📄 SecurityContextHolder - 인증한 대상에 대한 상세 정보를 담는다.
  • 📗 SecurityContext - SecurityContextHolder에 접근할 수 있으며, 현재 인증한 사용자의 Authentication을 가진다.
  • 📗 Authentication - 사용자가 제공한 credential이나 SecurityContext에 있는 사용자의 credential을 제공하며, AuthenticationManager의 입력으로 사용한다.
  • 📗 GrantedAuthority - 접근 주체(principal)에게 부여된 권한이다. (role)
  • 📗 AuthenticationManager - 필터가 인증을 어떻게 수행할지 정의하는 API이다.
  • 📄 ProviderManager - AuthenticationManager의 일반적인 구현체이다.
  • 📗 AuthenticationProvider - ProviderManager가 특정 유형의 인증을 수행하는 데에 사용한다.
  • 📗 AuthenticationEntryPoint - 클라이언트로부터 credential을 요청하는 데에 사용한다.
  • 📄 AbstractAuthenticationProcessingFilter - 인증에 사용할 Filter의 베이스이다.

 

🍎 SecurityContext & SecurityContextHolder

  스프링 시큐리티 인증 모델의 중심이다. 인증한 사용자의 상세 정보를 저장한다. 인증된 주체(principal)의 정보를 얻어야 한다면 여기로 접근하면 된다.

  • SecurityContext: 인증된 Authentication 객체를 저장하는 컴포넌트
  • SecurityContextHolder: SecurityContext를 관리
  • SecurityContextHolder에 의해 SecurityContext에 값이 채워져 있다면 인증된 사용자로 간주한다.

🍎 Authentication

  AuthenticationManager의 입력으로 사용되어, 인증에 필요한 사용자의 credential을 제공한다. 현재 Authentication은 SecurityContext에서 가져올 수 있다.

  • Principal: 사용자를 식별한다. 사용자 이름 / 비밀번호로 인증할 땐 보통 UserDetails 인스턴스다.
  • Credentials: 주로 비밀번호이다. 유출되지 않도록 사용자를 인증한 뒤 ProviderManager가 이를 비운다.
  • Authorities: AuthenticationProvider에 의해 사용자에게 부여한 권한은 GrantedAuthority로 추상화한다. (예: role, scope)

 

🍎 GrantedAuthority

  애플리케이션 전체에 걸친 인증한 주체(Principal)에게 부여된 권한이다. Authentication.getAuthorities() 메소드로 접근할 수 있다. 이는 GrantedAuthority 객체의 Collection을 리턴 한다. 보통 ROLE_USER 같은 역할이다. 이름 / 비밀번호 기반의 인증을 사용한다면 보통 UserDetailService가 이를 로드한다.

 

🍎 AuthenticationManager & ProviderManager

  AuthenticationManager는 스프링 시큐리티 필터의 인증 수행 방식을 정의하는 API이다. 이를 통해 느슨한 결합을 유지하며, 인증을 위한 실질적인 관리는 구현 클래스를 통해 이뤄진다. 리턴한 Authentication을 SecurityContextHolder에 설정하는 건 이를 호출한 객체(필터)가 담당한다.

  ProviderManager는 가장 많이 쓰이는 AuthenticationManager의 구현체다. ProviderManager는 동작을 AuthenticationProvider List에 위임한다. 모든 AuthenticationProvider는 인증을 성공 혹은 실패시키거나, 결정을 내릴 수 없는 것으로 판단하고 다음 스트림에 있는 AuthenticationProvider가 결정하도록 만들 수 있다. 모두 인증하지 못하면 ProviderNotFoundException[각주:1]과 함께 실패한다.

  보통은 AuthenticationProvider마다 각자가 맡은 인증을 수행하는 방법을 알고 있다. 예를 들어 어떠한 하나로 이름 / 비밀번호를 검증할 수 있고, 다른 하나는 SAML 인증을 담당할 수 있다. 때문에 AuthenticationManager 빈 하나만 노출하면서도 여러 인증 유형을 지원할 수 있다.

기본적으로 ProviderManager는 인증에 성공하면 반환받은 Authentication 객체의 민감한 credential 정보를 지운다. 이로써 비밀번호 같은 정보를 HttpSession에 필요 이상으로 길게 유지하지 않는다.

하지만 상태가 없는 어플리케이션에서 성능 향상을 위해 사용자 객체를 캐시한다면 문제가 된다. Authentication이 캐시 안에 있는 객체(UserDetails 등)을 참조하고 있는데 credential을 제거한다면 캐시한 값으로는 더 이상 인증할 수 없다. 이를 고려해서 Authentication 객체를 생성하는 AuthenticationProvider에서 객체의 복사본을 만들면 해결된다.

 

🍎 AuthenticationProvider

  ProviderManager에는 이를 여러 개 주입할 수 있다. AuthenticationManager로부터 인증 처리를 위임받아서 실질적인 인증 수행을 담당한다. AuthenticationProvider마다 담당하는 인증 유형이 다르다. 예를 들어서 DaoAuthenticationProvider는 이름 / 비밀번호 기반 인증을, JwtAuthenticationProvider는 JWT 토큰 인증을 UserDetailsService로부터 전달받은 UserDetails로 지원한다.

  • UserDetails: 사용자 자격을 증명해 주는 크리덴션이다.
  • UserDetailsService: UserDetails를 로드 하는 핵심 인터페이스이다. 사용자 정보를 메모리에서 로드하든 DB에서 로드하든 Spring Security가 이해할 수 있는 UserDetails로 리턴한다.

 

🍎 AuthenticationEntryPoint

  클라이언트의 credential을 요청하는 HTTP 응답을 보낼 때 사용한다. 클라이언트가 요청을 보낼 때 미리 이름 / 비밀번호 같은 credential을 보낼 때도 있는데, 이럴 땐 만들 필요가 없다. 그렇지 않을 경우에는 AuthenticationEntryPoint의 구현체가 클라이언트에게 credential을 요청한다. (로그인 페이지로 리다이렉트 혹은 WWW-Authenticate 헤더로 응답 등)

 

🍎 AbstractAuthenticationProcessingFilter

  사용자의 credential을 인증하기 위한 베이스 Filter이다. 필터 역할을 하기 위한 doFilter()를 포함한다. HTTP 기반의 인증 요청을 처리하며, credential을 인증할 수 없다면, 스프링 시큐리티는 보통 AuthenticationEntryPoint로 credentical을 요청한다.

인증 시도는 하위 클래스에게 맡기고, 인증에 성공하면 사용자 정보를 SecurityContext에 저장한다. - UsernamePasswordAuthenticationFilter가 상속하는 상위 클래스이다.

  1. 사용자가 credential을 제출하면 인증할 HttpServletRequest로부터 Authentication을 만든다. 생성하는 Authentication 타입은 AbstractAuthenticationProcessingFilter 하위 클래스에 따라 다르다. 예시로 usernamepasswordauthenticationfilterHttpServletRequest에 있는 usernamepasswordUsernamePasswordAuthenticationToken을 생성한다.
  2. AuthenticationAuthenticationManager로 넘겨서 인증한다.
  3. 인증에 실패할 경우
    1. SecurityContextHolder를 비운다.
    2. AuthenticationFailureHandler를 실행한다.
  4. 인증에 성공할 경우
    1. SessionAuthenticationStrategy에 새로 로그인했음을 통보한다.
    2. SecurityContextHolderAuthentication을 세팅한다. 이후 SecurityContextPersistenceFilterHttpSessionSecurityContext를 저장한다.
    3. ApplicationEventPublisherInteractiveAuthenticationSuccessEvent를 발생시킨다.

 

🍎 Username / Password Authentication

  사용자 이름과 비밀번호 검증은 사용자 인증 시에 많이 사용되는 방법이다. 스프링 시큐리티는 HttpServletRequest에서 이름과 비밀번호를 읽을 수 있도록 폼 로그인 / 기본 인증 / 다이제스트 인증 3가지 방법을 제공한다. 조회 메커니즘은 저장 메커니즘 - 인메모리 인증과 심플 스토리지 / JDBC 인증과 관계형 데이터베이스 / UserDetailsService와 커스텀 데이터 스토어 / LDAP 인증과 LDAP 스토리지 - 중 어떤 것과도 조합할 수 있다.

Basic Authentication

  스프링 시큐리티에선 HTTP 기본 인증을 디폴트로 활성화한다. 어떻게 서블릿 기반 어플리케이션에서 기본 HTTP 인증을 지원할까?

SecurtiyFilterChain 기반

  1. 사용자가 권한이 없는 리소스에 요청을 보낸다.
  2. FilterSecurityInterceptor에서 AccessDeniedException을 던져서 거절한다.
  3. 인증되지 않은 사용자이므로 ExceptionTranslationFilter에서 인증을 시작한다. 설정한 AuthenticationEntryPoint는 BasicAuthenticationEntriyPoint 인스턴스로, WWW-Authentication 헤더를 전송한다.

SecuritiFilterChain 다이어그램 기반

  1. 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilterHttpServletRequest에서 이 값을 추출해 Authentication 유형 중 하나인 UsernamePasswordAuthenticationToken을 만든다.
  2. UsernamePasswordAuthenticationTokenAuthenticationManager로 넘겨 인증한다. 상세 동작은 사용자 정보를 저장한 방식에 따라 다르다.
  3. 인증에 실패할 경우
    1. SecurityContextHolder를 비운다.
    2. AuthenticationEntryPoint를 실행해서 WWW-Authentication 전송을 트리거한다.
  4. 인증에 성공할 경우
    1. SecurityContextHolderAuthentication을 세팅한다.
    2. BasicAuthenticationFilter에서 FilterChain.doFilter(request, response)를 호출해서 나머지 어플리케이션 로직을 실행한다.
  • UsernamePasswordAuthenticationFilter: Spring Security Filter Chain에서 사용자 로그인 요청을 제일 먼저 만나는 컴포넌트이다. Username과 Password를 통한 인증 처리를 위해 UsernamePasswordAuthenticationToken을 생성한다. 필터 역할을 하기 위한 doFilter()를 포함하는 AdstractAuthenticationProcessingFilter를 상속한다.
  • UsernamePasswordAuthenticationToken: Username과 Password로 인증을 수행하기 위해 필요한 토큰이다. 인증 성공 후 사용자 정보가 토큰에 포함되어 Authentication 객체 형태로 SecurityContext에 저장된다.

 


 

 

  1. 클라이언트가 사용자 정보를 가지고 로그인 요청을 보낸다.
  2. AuthenticationFilter가 이를 가로채서 Authentication를 생성한다. 
    1. UsernamePasswordAuthenticationFilter
    2. UsernamePasswordAuthenticationToken을 생성한다. (인증 수행 도구)
  3. 필터는 아직 인증되지 않은 인증 정보를 AuthenticationManager의 구현체인 ProviderManager에게 전달한다.
    1. Authentication
    2. UsernamePasswordAuthenticationToken
  4. 이제 DB에 담긴 사용자 인증 정보와 비교하기 위해 AuthenticationProvider에게 전달한다.
  5. UserDetailsService를 거쳐서 사용자 정보를 조회하고, 사용자 정보인 UserDetails 객체를 생성한다.
  6. AuthenticationProviderPasswordEncoder을 통해 UserDetailsAuthentication의 패스워드를 비교한다.

  7. 인증에 성공하면 권한과 사용자 정보를 담은 Authentication을 생성하고 그렇지 않다면 예외를 발생하고 처리를 중단한다.
  8. AuthenticationFilter(UsernamePasswordAuthenticationFilter)까지 Authentication를 전달한다.

  9. 세션 정책에 따라 HttpSession에 저장되어 상태를 유지하기도 한다.→ JWT
    SecurityContextHoder로 SecurityContext에 인증된 Authentication 저장한다.

 

DaoAuthenticationProvider와 AbstractUserDetailsAuthenticationProvider의 메서드가 호출되는 순서

  1. AbstractUserDetailsAuthenticationProvider의 authenticated() 메서드 호출
  2. DaoAuthenticationProvider의 retrieveUser() 메서드 호출
  3. DaoAuthenticationProvider의 additionalAuthenticationChecks() 메서드 호출
  4. DaoAuthenticationProvider 의 createSuccessAuthentication() 메서드 호출
  5. AbstractUserDetailsAuthenticationProvider 의 createSuccessAuthentication() 메서드 호출
  6. 인증된 Authentication을 ProviderManager에게 리턴

 


📚 Reference

 

  1. AuthenticationException의 하위 클래스로, 넘겨진 Authentication 유형을 지원하는 ProviderManager를 설정하지 않았음을 의미한다. [본문으로]

댓글