Basic 로그인 모듈

갈아만든배·2022년 12월 26일
0

로그인 모듈 개발

간단한 핵심 기능만 구현되어 있는 모듈을 개발하며 해당 모듈의 핵심을 파악해보려고 합니다.

개인 공부를 위해 진행한 토이프로젝트를 분석하며 정리한 내용이므로 혹여 틀린 부분이 있다면
덧글로 알려주시면 감사하겠습니다.

시나리오

사용자는 로그인을 위해 '/public/login' 페이지로 최초 진입합니다.

사용자는 자신의 계정 정보를 입력하고 로그인 버튼을 클릭합니다.

로그인이 완료된 사용자는 /private/success 페이지로 이동됩니다.

'/public/**' 경로는 모든 사용자가 접근할 수 있으며,

그 외의 경로는 인증된 사용자만 접근할 수 있습니다.

소스 구성

로그인 필터 체인 구성

SecurityFilterChain bean을 생성해 기본적인 URL Mapping을 설정합니다.

  1. GET method로 전달되는 '/public/' URL은 permitAll로 설정합니다.
    그 외의 Request는 모두 인증이 필요하게끔 설정합니다.

  2. 로그인 페이지는 '/public/login' 경로로 연결됩니다.

  3. 추후 로그아웃 관련 로직을 처리할 로그아웃 핸들러로
    CustomLogoutHandler Object를 등록합니다.

  1. 사용자의 인증 정보는 사용자의 request에 쿠키 형식으로 저장하고 별도의 세션은 가지고 있지
    않을 예정이므로, 세션 생성 정책을 STATELESS로 설정합니다.

  2. 로그인 관련 로직을 처리할 필터인 LoginFilter 객체를 등록합니다.

  • 로그인 진입 URL : /doLogin.
  • 필터 종류 : UsernamePasswordAuthenticationFilter
  1. 사용자 인가 관련 로직을 처리할 필터인 AuthenticationFilter를 등록합니다.
  • 인가가 필요한 URL : '/public/'하위 경로를 제외한 모든 URL이나 현재 시나리오 상에서는
    '/private/' 하위 경로입니다.

디버깅

실제 로그인 요청이 올 시에 모듈이 작동하는 소스를 디버깅 해보면서 소스의 동작 순서 및 세부 동작 로직을 정리해봅시다.

  1. 사용자는 로그인 시도를 위해 '/public/login' 경로로 접속합니다.

  1. 이후 사용자는 본인의 계정을 입력하고 로그인 버튼을 클릭합니다.

  2. 로그인 버튼 클릭 시 필터에 설정한 로그인 진입 URL인 /doLogin으로 사용자가 입력한
    계정 정보를 전송합니다.

  3. 서버는 사용자의 인증 로직을 처리할 필터로 LoginFilter라는 이름의 커스텀 필터를 구현해
    UserPasswordAuthenticationFilter 전에 등록해 두었습니다.

  • 로그인 필터는 AbstractAuthenticationProcessingFilter 클래스를 상속받아 구현하였으므로,
    이후 디버깅 시 부모 클래스의 메소드나 변수가 나올 수 있습니다.
  1. Spring Security는 Filter chain을 구성하고 chain 순서에 맞게 각각의 Filter를
    순차적으로 처리하는데, 이 중 사용자 인증을 담당하는 UserPasswordAuthenticationFilter 순서에
    등록한 LoginFilter에서 사용자 인증 요청이 전달되어 처리되게 됩니다.
  • doFilter() 메소드에 의해 인증이 필요한지 여부를 비교하고 인증이 필요할 경우
    attemptAuthentication() 메소드를 실행합니다.

  • 이후 인증 처리가 완료될 경우, successfulAuthentication() 메소드를 실행합니다.

  • LoginFilter에서는 부모 클래스에서 실행된 attemptAuthentication() 메소드를 통해
    request와 response를 전달 받아 실행됩니다. (authenticate() 메소드는 하단에 따로 서술)

  1. 전달받은 request에서 사용자가 전달한 계정 정보를 가져옵니다.
    (현재 소스에서는 username/password).
  • 파라미터 명은 form의 input name으로 설정해 두었으므로 해당 이름을 key로 조회합니다.

  1. 이후 객체 생성 시 전달받은 authenticationManager에게 인증 시 실제 사용자 정보와 비교할 때
    사용할 임시 토큰을 생성해 파라미터로 전달하며 authenticate() 메소드를 실행합니다.
  • authenticationManager는 SecurityConfig의 getAuthManager() 메소드를 통해
    생성 시 전달받았습니다.


  • getAuthManager() 메소드는 내부에서 auth의 getOrBuild() 메소드의 return 값을
    반환합니다.

  • getOrBuild() 메소드는 내부에서 현재 클래스가 빌드되었는지 체크 후 빌드가 되어 있으면
    해당 객체를, 빌드가 되어있지 않으면 빌드 후 해당 결괏값을 반환합니다.

  • 이때, build() 메소드에서는 doBuild() 메소드를 실행 후 해당 결과를 object 멤버 변수에
    담아 반환하도록 개발되어 있습니다.

  • doBuild() 메소드는 synchronized 키워드를 통해 스레드 동기화를 진행한 상태로 메소드를
    실행하고, init() 및 관련 configure() 메소드들을 순차적으로 실행한 후,
    performBuild() 메소드의 결괏값을 반환합니다.

  • performBuild() 메소드 내부에서는 providerManager 객체를 생성해 반환합니다.

  • 이 때 Component로 등록해두었던 CustomAuthProvider가 AuthBuilder를 Bean으로 주입 할 때 함께 주입되며 결과적으로 providerManager 객체의 생성자에 전달됩니다.

  1. filter에 전달된 authenticationManager 객체는 결과적으로 providerManager 객체를
    담고있으므로, providerManager.authenticate() 메소드가 실행되게 됩니다.

  • authenticate() 메소드에서는 생성자로 전달받은 providers 멤버 변수의 iterator를
    var9 변수에 저장합니다.

  • 이후 iterator에서 provider 객체를 하나 꺼내어 처리할 준비를 합니다.

  • iterator에서 꺼내어진 provider 객체의 authenticate() 메소드를 실행한 후 결괏값이 존재할
    경우, result 변수에 담아 반환하고 반복문을 종료합니다.

  • 해당 부분에서 같이 providerManager 객체에 전달되었던 CustomAuthProvider의 부모 메소드인 AbstractUserDetailsAuthenticationProvider의 authenticate() 메소드가 실행되게 됩니다.

  • determineUsername() 메소드를 실행한 후 결괏값으로 username을 반환받습니다.
    이때 반환되는 것은 사용자가 로그인 시도 시 입력한 계정 id가 됩니다.

  • 이후 userCache를 이용해 캐시된 사용자 목록에 존재하는지 확인하는데, 해당 프로젝트는 캐시 설정을
    별도로 하지 않았으므로 NullUserCache 객체를 담고 있는 userCache는 null값을 반환합니다.

  • userCache의 반환 값이 null이므로 retrieveUser() 메소드를 실행한 후 반환값을 user에
    저장합니다.

  • retrieveUser() 메소드는 함께 전달받았던 userDetailsService를 이용해 해당 계정을 가진
    userDetails가 존재하는지 확인 후 존재할 경우 userDetails를 반환합니다.

  • 이후 preAuthenticationChecks 변수에 저장되어 있던 DefaultPreAuthenticationChecks
    객체를 이용해 기본적인 userDetails의 체크를 진행한 후, 통과되면 authProvider의
    additionalAuthenticationChecks() 메소드에 userDetails를 전달합니다.

  • 전달 이후 DefaultPostAuthenticationChecks 객체의 check() 메소드를 실행한 후
    마찬가지로 체크 통과 시 createSuccessAuthentication() 메소드를 실행합니다.

  • 인증이 완료된 userDetails를 이용해서 UsernamePasswordAuthenticationToken를
    생성해 토큰을 반환합니다.

  1. 위에서 초기에 분석한 대로 provider에서 Authentication 객체가 반환되었으므로,
    LoginFilter의 successfulAuthentication() 메소드가 실행되게 됩니다.

  • 인증 성공 처리를 위해 생성된 인증 결과인 authResult를 쿠키에 담아 사용자에게 전달할 준비를 합니다.

  • 이후 쿠키를 포함한 response와 함께 사용자를 인증 성공 페이지("/private/success")로
    rediect 시켜주며 로그인에 대한 처리를 완료합니다.

결론

현재까지 간단한 모듈 개발을 통해 Spring Security에서 로그인을 시도할 때 어떤 클래스와 메소드들을
이용해 처리해주는지 간략하게 살펴보았습니다.

사실, 상속받아 직접 구현하는 부분은 극히 일부분이고 그 이외의 부분들은 Spring에 추상 클래스들을 통해
이미 어느정도 개발되어 로직이 이어지기 때문에 위에 소스처럼 LoginFilter의 Overriding을 진행한
부분만 구현하여도 기본적인 로그인 기능은 제공할 수 있습니다만,

그래도 해당 부분을 조금이라도 알고 있으면 도움이 될까 싶어 디버깅 및 소스 분석 연습 겸 간단한 로그인
모듈을 구현하여 개발해보았습니다.

사용자 인가 부분은 다음 글에 작성될 예정입니다.

해당 소스는 언젠가 제가 git 공부를 조금 더 하고 git 사용법을 정리할 때 올라갈 예정입니다.

혹시 틀린 부분이나 수정하면 좋을법한 부분이 보인다면 해당 부분을 댓글로 남겨주세요.
댓글로 남겨주신 부분에 대해서는 더 공부한 후에 해당 글에 추가하거나 별도의 글로 또 남겨두겠습니다.

0개의 댓글