• SpringSecurity2 - Authentication Architecture

    2022. 7. 13.

    by. 내이름은 킹햄찌

    SpringSecurity doc

    SpringSecurity문서의 가장 상단에 위치하는 내용이다.

    이를 번역해보자면 아래와 같다.

     

    서블릿 인증에 사용되는 Spring Security의 주요 아키텍처 구성 요소를 설명하기 위해 Servlet Security의 큰 그림을 확장 시킨 내용이다. 이러한 부분이 어떻게 서로 맞는지 설명하는 구체적인 흐름이 필요한 경우 인증 메커니즘 관련 섹션을 살펴보라.

     

    SecurityContextHolder : 인증된 사람들을 저장하는 SpringSecurity의 저장소이다.

    SecurityContext : SecurityContextHolder에 포함되어 있고 인증된 유저의 Authentication 을 포함한다.

    Authentication : AuthenticationManager에 제공하기 위한 인증받은 유저이거나 SecurityContext 안에 있는 유저의 자격증명이 될 수 있다.

    GrantedAuthority : 보안주체에 부여된 권한입니다.(ex : roles, scope)

    AuthenticationManager : 어떻게 SpringSecurity API가 인증을하는 방법 정의한다

    ProviderManager : AuthenticationManager의 구현체이다.

    AuthenticationProvider : 특정 인증방법을 수행하고 ProviderManager 에 의해 사용된다. Request Credentials with AuthenticationEntryPoint : 클라이언트로부터 자격증명을 요청하는데 사용됨(ex : 로그인페이지로 리다이렉션, www - 인증 응답전송 등)

    AbstractAuthenticationProcessingFilter : 인증에 사용되는 Filter의 기본이다. 이것은 또한 높은 수준의 인증 흐름과 조각들이 함께 작동하는 방식에 대한 좋은 아이디어를 제공한다.

     

    문서 전체를 읽었지만 추상적인 느낌이 나고 임팩트있게 와닿지 않았다.

    그래서 본인의 컨디션 난조인가 싶기도 해서 머리속에 무수한 물음표들이 난무한체로 블로그 들을 참고하기 시작했는데 아래의 그림을 보니 물음표들이 빳빳하게 머리를 들며 느낌표가 되는 느낌을 받았다.

     

     

    https://mangkyu.tistory.com/76

    SpringSecurity라는 키워드로 검색을 하면 대부분 해당 그림으로 소개를 하는 것을 볼 수 있었다.

    정리가 너무 잘된 그림이라서 설명을 조금 더 보고 싶어 아래 참조 되어 있던 www.springbootdev.com 으로 들어 갔지만 페이지는 존재하지 않았다.

    SpringSecurity 공식문서에서 읽었던 내용들이 맞아 떨어지는 느낌이 들기때문에 코드를 따라가며 검증겸 이해하는 과정을 공유하도록 하겠다.

     

    인증되지 않은 사용자를 인증할때

     

    1. AuthenticationFilter

    여기서 포괄적으로 말하는 Filter는 문서에서 말하는 AbstractAuthenticationProcessingFilter를 의미하는 것으로 보인다.

    AbstractAuthenticationProcessingFilter

    위는 AbstractAuthenticationProcessingFilter의 doFilter 메서드 코드이다.

    doFilter 는 FilterChainProxy에서 Filter를 호출할때 사용하게되는데 아래에 성공했을때, 실패했을때, Exception 발생시에 대한 로직이 있고 중요한건 빨간색으로 표시한 부분이다. 여기에 있는 attemptAuthentication에 대한 구현이 UsernamePasswordAuthenticationFilter에 되어있고, 2번 내용에 해당되게 된다.

     

    2. UsernamePasswordAuthentiationToken

    UsernamePasswordAuthenticationFilter

    가로첸 request가 POST인지 확인 후에 username과 password 파라미터를 토큰에 담아서 AuthentiationManager에서 인증을 하게된다. 여기서 주의할점은 코드를 보면 알 수 있지만 request에서 넘어오는 파라미터의 key값이 username, password가 아닐경우에는 value를 인식하지 못하게 된다. 이 내용은 설정으로 바꿀 수 있으니 관련 내용은 직접 찾아보면 된다.

    간단하게 UsernamePasswordAuthentiationToken를 살펴보자

     

    UsernamePasswordAuthentiationToken

    위에서 사용한 생성자이다. username과 password를 담고, setAuthenticated를(false)로 생성을 한다.

    UsernamePasswordAuthentiationToken의 부모인 AbstractAuthenticationToken 이 Authentication을 상속 받고 있기때문에 해당 토큰이 인증 되었을때 setAuthenticated(true)가 되어 SecurityContext에 담겨질 것으로 보인다.

     

    3,4 . AuthenticationManager를 상속 받은 ProviderManager 내부에 있는 AuthenticationProvider

    ProviderManager

    위는 구현체인 ProviderManager의 코드의 authenticate메서드의 일부이다.

    AuthenticationManager은 Authentication을 관리하고 있지만 인증은 ProviderManager에게 위임하고 있고 ProviderManager는 내부에서 직접 인증을 하는 것이 아니고 등록되어 있는 AuthenticationProvider(인증 방법)에게 authentication(UsernamePasswordAuthentiationToken) 을 넘겨 인증을 하게 된다.

     

    5. UserDetailsService,AbstractUserDetailsAuthenticationProvider

    AuthenticationProvider에 기본으로 등록되어 있는 AbstractUserDetailsAuthenticationProvider 를 살펴보자

    AbstractUserDetailsAuthenticationProvider

    Authentication의 기본 구현체인 AbstractUserDetailsAuthenticationProvider 는 ProviderManager에 의해 authenticate 매서드를 실행하게 되는데 여기서 중요한 부분이 빨간색으로 표시한 retriveUser과 additionalAuthenticationChecks 매서드이다. 둘다 추상클래스로 AbstractUserDetailsAuthenticationProvider 를 상속받고 있는 DaoAuthenticationProvider에 매서드에 구현이 있다.

     

    DaoAuthenticationProvider

    DaoAuthenticationProvider에 있는 retrieveUser 메서드 구현이다.

    빨간색으로 표시한 부분이 username을 바탕으로 user의 정보를 조회하는 부분이다.

    SpringSecurity에서는 UserDetailsService를 상속 받은 JdbcDaoImpl(JDBC 이용), CachingUserDetailsService(In-Memory 이용) 구현체를 사용할 수 있다. 다만 JdbcDaoImpl를 이용한 인증을 하게 될 경우에는 SpringSecurity문서에서 정의되어있는 user테이블과 authorities테이블을 양식에 맞추어 사용해야한다. Jpa를 사용하여 개발할 경우에는 UserDetailsService을 상속받아서 CosutomUserDetailsService에서 repository를 이용한 인증도 할 수 있다. 이 부분은 다음글에서 커스텀한 인증에서 볼 수 있을 것이다.

    DaoAuthenticationProvider

    DaoAuthenticationProvider에 있는 additionalAuthenticationChecks메서드 구현이다.

    빨간색 부분을 보게되면 사용자의 인증을 위해 비밀번호를 비교하는 부분이 나오는데 PasswordEncoder가 등장한다. PasswordEndcoder에 대해 문서에 정리된 내용을 보자면 패스워드를 단방향으로 변환하여 안전하게 저장할 수 있게 해주는 인터페이스이다. DaoAuthenticationProvider를 사용한다면 Default가 PasswordEncoderFactories 이기 때문에 반드시 암호화의 방법을 정의해주어야 한다.

     

    PasswordEncoderFactories

    PasswordEncoderFactories 의 createDelegatingPasswordEncoder메서드를 보면 어떤 암호화의 방법들이 있는지 확인할 수 있고 그와 관련된 내용들은 위에 PasswordEndcoder관련 링크로 걸려있는 문서에서 읽을 수 있다.

    UserDetailsService에서 정의한 인증 방법이 끝나게 되면 AbstractUserDetailsAuthenticationProvider authenticate메서드의 아래쪽에서 인증된 토큰을 반환하게 된다. 해당 매서드를 살펴보면

     

    AbstractUserDetailsAuthenticationProvider

    UsernamePasswordAuthenticationToken의 생성자를 호출하는데

    UsernamePasswordAuthenticationToken

    setAuthenticated(ture)로 인증되어 있는 토큰이 반환될것으로 예상할 수 있다.

     

    6. UserDetails

    UserDetails는 SpringSecurity에서 사용자의 정보를 담는 인터페이스이다. 구현체를 만들어서 UserDetailsService에서 반환 타입으로 사용하게 된다.

    UserDetails

    메서드 위에 설명이 어느정도 되어 있기 때문에 따로 설명하지 않고 번역이 필요하다면 문서를 번역하여 읽어 보길 권장한다.

     

    7~9. AuthenticationFilter

    만약 정상적으로 인증이 되었다면 successfulAuthentication가 실행될 것이고 그렇지 않을 경우 아래 부분이 실행이 될 것이다.

     

    10.successfulAuthentication

    successfulAuthentication메서드 구현를 보게되면 SecurityContext에 인증된 authentication을 넣고 SecurityContextHolder에 SecurityContext를 set하는것을 볼 수 있다.

    로그인을 통해 사용자는 인증된 사용자가 되었다.

    이렇게 인증된 사용자는 다른 페이지로 이동했을때 인증된 사용자라는 것을 알릴 수 있을까

    해답은 SecurityContextPersistenceFilter에서 알 수 있다.

     

     

     

    인증된 사용자가 리소스에 접근할때

    SecurityContextPersistenceFilter

    Filter의 이름에서 알수있듯 SecurityContext를 Persistence하는 Filter라는 것을 알 수 있다.

    어떻게 영속화를 하게 될까

    빨간색으로 표시한 부분중 첫번째에 보면 SecurityContext load하여 SecurityContextHolder에 넣는 것을 볼 수 있다.

    여기의 repo는 SecurityContextRepository를 상속받은 HttpSessionSecurityContextRepository를 표현 한것이다. loadContext는 어떤 방식으로 SecurityContext를 가지고 오는 것일까

     

    loadContext메서드의 구현을 보면 Session에서 SecurityContext를 꺼내온다는것을 알 수 있다.

    다시 SecurityContextPersistenceFilter로 돌아가면 이제 남은 로직은 SecurityContextHolder에 SecurityContext를 넣어주는데 이것만으로 어떻게 영속화가 될 수 있을까

     

    SecurityContextHolder는 SecurityContextHolderStrategy를 맴버로 가지는데 이 strategy에 따라 전략이 달라 질 수 있는데 특별한 설정을 하지 않은 Default값은 ThreadLocalSecurityContextHolderStrategy로 되어있다.

     

    ThreadLocalSecurityContextHolderStrategy는 내부에 맴버로 ThreadLocal을 가지고 있는 것을 볼 수 있다. 이렇게 되면 해당 요청 동안에는 ThreadLocal에는 저장된 SecurityContextHolder를 이용하여 내부에 있는 Principal을 꺼내 사용할 수 있게 되는것이다.

     

    정리

    인증되지 않은 사용자를 인증할때

    1. Filter( ****UsernamePasswordAuthenticationFilter) 에서 Request에 있는 Username과 Password를 UsernamePasswordAuthentiationToken에 담아서 AuthenticationManager으로 전달한다
    2. AuthenticationManager은 내부의 ProviderManager을 통하여 하게되는데 ProviderManager은AuthenticationProvider(를 상속받은 구현체)에게 인증을 위임한다.
    3. AuthenticationProvider에서 인증을 하기위해 UserDetailsService(를 구현한 구현체)가 유저정보를 가지고 온다.
    4. 유저정보는 UserDetails(를 구현한 구현체)이다.
    5. AuthenticationProvider에서 인증이 완료되면 다시 **Filter로 Authentication(**UsernamePasswordAuthentiationToken)를 보내주고 filter에서는 이를 SecurityContextHorder에 넣는다.

    인증된 사용자가 리소스에 접근할때

    1. SecurityContextPersistenceFilter에서 session을 이용하여 SecurityContext를 찾는다.
    2. SecurityContextHolder에 SecurityContext를 넣는다. 이때 SecurityContextHolder가 LocalThread이기 때문에 요청이 반환되기까지 언제든 Principal을 사용할 수 있게 된다.

    댓글