[동아리 프로젝트 로그인 기능 구현] SpringSecurity 동작 방식1

박준수·2022년 11월 15일

들어가기전에...

2학년 2학기 코부엉이라는 코딩동아리에 들어가서 처음으로 프로젝트에 참여하게 되었다. 50명이 넘는 인원이 프로젝트에 참여중이고 나는 백엔드 부분에서 Login 기능을 구현하는 담당을 맡았다. 프로젝트에 들어가니 나는 아무것도 할 줄 아는게 없었고 부회장님이 말한 가장 어려운 부분(Spring Security, JWT)을 맡았다. 내가 왜 부분을 맡을려 했는지... 패기만 있었다ㅋㅋㅋ....
결국 친구 한명과 구글링을 엄청하면서 괜찮은 예제를 찾고 복붙을 해버렸다ㅋㅋㅋ. 부회장님도 우리가 새싹이라는 것을 알고 복붙을 이해해주셨고 그래도 우리가 맡은 부분에 대해 어느 정도는 알고 있어야 하지 않겠냐는 말에 복붙한 코드를 분석하고 구글링한것을 정리하려한다.

스프링 시큐리티(Spring Security)

: 스프링 기반 어플리케이션의 보안(인증과 권한, 인가)를 담당하는 스프링 하위 프레임워크이다.

  • 보안과 관련해서 체계적으로 많은 옵션들을 제공해주기 때문에 개발자의 입장에서는 하나하나 보안 관련 로직을 작성하지 않아도 된다는 장점이 있다.

Spring Security 기본 용어 및 이론

  • 인증(Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차
  • 인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차
  • 접근 주체(Principal) : 보호받는 Resource에 접근하는 대상
  • 비밀번호(Credential) : Resource에 접근하는 대상의 비밀번호
  • 권한 : 인증된 주체가 어플리케이션의 동작을 수행할 수 있도록 허락되어 있는지를 결정합니다.
    • 인증 과정을 통해 주체가 증명된 이후 권한을 부여할 수 있다.
    • 권한 부여에도 두가지 영역이 존재하는데 웹 요청 권한과 메서드 호출 및 도메인 인스턴스에 대한 접근 권한 부여가 있다.

인증(Authentication) -> 인증 성공 후 -> 인가(Authorization)

Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지를 확인하게 됩니다. 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 인증 방식을 사용합니다.

스프링 시큐리티 전체적인 동작 과정

처음부터 자세히 들어가보자!!

HTTP 요청 수신

Security에는 일련의 필터 체인이 있습니다. 그래서 클라이언트(브라우저)로 부터 요청(Request)이 오면 인증 및 권한 부여 목적으로 일련의 필터를 거치게 됩니다.
일반적으로 ID, PASSWORD 기반의 인증이라고 할 경우 가장 먼저 Application Filters라는 필터 뭉치에 도달합니다. 그리고 그 필터들 중 Authentication Filters라는 필터 뭉치에 다시 도달합니다. 다음으로 username, password를 사용하는 form 기반 인증을 처리하는 필터인 UsernamePasswordAuthenticationFilter에 도착하게 됩니다.

  • id, password가 아닌 OAuth2.0 인증이나 JWT를 이용한 인증을 하려고 할 때는 해당 필터가 아닌 다른 필터를 거치게 됩니다.

UsernamePasswoardAuthenticationFilter에 요청이 도착하면 해당 클래스의 attempAuthentication(request, response) 메서드가 동작합니다. 이 메서드는 request로부터 username, password를 가지고 와서 사용자 자격 증명을 기반으로 한 UsernamePasswordAuthenticationToken(Authentication)을 생성합니다.

이어서 생성된 UsernamePasswordAuthenticationToken(Authentication)을 가지고 AuthenticationManager (실질적으로는 구현체인 ProviderManager)에게 인증을 진행하도록 위임합니다.

  • UsernamePasswordAuthenticationToken은 Authentication 인터페이스의 구현체입니다. 객체 간의 구조를 뜯어보면 UsernamePasswordAuthenticationToken이 AbstractAuthenticationToken을 extends, AbstractAuthenticationToken은 Authentication을 implements 하는 구조로 설계되어 있습니다.
  • 모든 접근 주체는 Authentication을 생성합니다. 이것은 최종적으로 SecurityContext에 보관되고 사용됩니다.

AuthenticationManager, AuthenticationProvicer(s), ProviderManager

  • AuthenticationManager(Interface)
    Authentication 객체를 받아 인증하고, 인증되었다면 인증된 Authentication 객체를 돌려주는 authenticate() 메서드를 구현하도록 하는 인터페이스입니다. 이 메서드를 통해 인증이 되면 isAuthenticated(boolean) 값을 true로 바꿔줍니다.

  • ProviderManager(Class)
    AuthenticationManager의 구현체로 스프링에서 인증을 담당하는 클래스로 볼 수 있습니다. 스프링 시큐리티가 생성, 등록하고 관리하는 스프링 빈이기 때문에 직접 구현할 필요는 없습니다.
    ProviderManager 클래스는 인증을 담당하고 있지만 실제로 직접 인증 과정을 진행하는 게 아니라 멤버 변수로 가지고 있는 AuthenticationProvider(s)에게 인증을 위임하고 그중에서 인증 처리가 가능한 AuthenticationProvider 객체가 인증 과정을 거쳐서 인증에 성공하면 요청에 대해 ProviderManager가 인증이 되었다고 알려주는 방식입니다.
    인증이 되었다고 알려주는 건 AuthenticationManager 인터페이스의 authenticate() 메서드의 리턴 값인 Authentication 객체 안에 인증 값을 넣어주는 것으로 처리합니다.

  • AuthenticationProvider(Inteface)
    AUthenticationManager와 같은 authenticate() 메서드를 통해 인증 과정이 진행됩니다.
    boolean supports(Class<?>) 메서드는 앞에서 필터를 통해 보내준 Authentication 객체를 이 AuthenticationProvider가 인증 처리가 가능한 클래스인지를 확인하는 메서드입니다.

중간 정리

  1. ID, PASSWORD 기반의 인증 요청을 UserPasswordAuthenticationFilter에서 가로챕니다.
  2. UsernamePasswordAuthenticationToken(Authentication) 객체를 AuthenticationManager에 넘깁니다.
    (실질적으로는 AuthenticationManager 인터페이스를 구현한 ProviderManager)
  3. ProviderManager는 여러 AuthenticationProvider를 순회하면서 UsernamePasswordAuthenticationToken을 처리해줄 AuthenticationProvider를 찾습니다.

AuthenticationProvider 인터페이스를 구현하는 클래스 만들기

실질적인 인증 로직을 위해 AuthenticationProvider interface를 구현한 클래스가 필요한데요. AuthenticationProvider를 implements 하게 되면 authenticate(), supports() 메서드를 구현하게 됩니다.


인증 절차를 통해 인증이 완료되면 Authentication을 SecurityContextHolder 객체 안의 SecurityContext에 저장한다.

Authentication authentication = SecutiryContextHolder.getContext().getAuthentication();

SecutiryContextHolder

  • SecutiryContextHolder
    SecurityContext 객체를 저장하고 감싸고 있는 wrapper 클래스로 SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 콘텍스트에 대한 세부 정보가 저장됩니다.
    (MODE_THREADLOCAL, MODE_INHERITABLETHREADLOCAL, MODE_GLOBAL 세 가지 저장 방식이 있습니다.)

  • SecutiryContext
    Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있습니다. ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계되어 있습니다.

마지막 정리

  1. 사용자가 로그인 정보와 함께 인증 요청(Http Request)
  2. AuthenticationFilter가 이 요청을 가로챕니다. 이 때 가로챈 정보를 통해 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성합니다.
  3. AuthenticationManager의 구현체인 ProviderManager에게 UsernamePasswordAuthenticationToken 객체를 전달합니다.
  4. 다시 AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체를 전달합니다.
  5. 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보(아이디)를 넘겨줍니다.
  6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다. 이 때 UserDetails 는 인증용 객체와 도메인용 객체를 분리하지 않고 인증용 객체에 상속해서 사용하기도 합니다.
  7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교합니다.
  8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.
  9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.
  10. Authentication 객체를 SecurityContext에 저장합니다.

참고 : 스프링 시큐리티 동작방식

profile
방구석개발자

0개의 댓글