[개발지식] (작성중) Web Application의 GateKeeper #2 - 요청에 대한 최초 진입점, 인증처리방식에 대한 비교분석

Hyo Kyun Lee·3일 전
0

개발지식

목록 보기
100/100

1. 개요

사용자 요청을 가장 앞단에서, 인증여부를 최초로 검사하기 위해 폼인증방식과 httpBasic(기본) 인증방식, 기억하기 인증방식을 사용한다.

각각의 인증방식의 세부적인 동작원리를 중점적으로 파헤쳐보면서 비교분석을 해보고, 사용자 요청이 어떻게 최초로 진입하고 이에 따라 인증이 이루어지는지 그 과정에 대해 기록해보고자 한다.

중요한 것은 "흐름"의 이해이다.

SpringSecurity는 작동방식에 대한 이해도 중요하지만, Spring Security의 흐름을 이해하고 이 흐름을 조절하기위한 API를 어떻게, 잘 활용하느냐도 중요하다.

이러한 흐름에 대해 개념적으로 이해하고, 나아가 실무에서 어떠한 요소를 중점적으로 활용할 것인지 혹은 어떠한 요소가 Spring Security 프레임워크에 어떠한 방식으로 작용하는지 등 API활용 방법을 숙지하는 과정으로 보면 좋을 것 같다.

2. 폼인증 방식(formLogin())

Spring Security는 Spring framework와 같은 프레임워크이기에, 인증이 필요한 부분에 대해 우리가 프로퍼티를 주입하거나, 이에 준하는 흐름 및 정보를 프레임워크에 전달해줄 필요가 있다.

formLogin은 그러한 Security 흐름을 적용해줄 하나의 인증방식이자, API이다.

일전에 SecurityBuilder의 구현체로 WebSecurity, HttpSecurity가 있다는 것을 알았다.

나아가, WebSecurity는 보안 적용 범위(무시/적용)를 설정하는 상위 Builder이고, HttpSecurity는 실제 SecurityFilterChain을 만드는 Builder이며, Builder(각 구현체들)는 Configurer들을 모으고 내부적으로 메소드들을 호출하여, init() → configure() → build() 단계로 호출, 최종적으로 보안 FilterChain을 완성한다.

           (SecurityBuilder)                     (SecurityBuilder)
   WebSecurity.build()   ─────────►   HttpSecurity.build()
             │                              │
             │                              │
      [Web Security Configurers]       [Http Security Configurers]
             │               init()            │             init()
             │               configure()       │             configure()
             │                              │
             ▼                              ▼
        (어떤 요청이 보안을 탈지)       (보안 필터들이 어떻게 동작할지)
                                             ↓
                                 SecurityFilterChain 생성
                                             ↓
      DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain → Filters

이때 각 Configurer내부의 메소드들에 대하여 다음과 같은 세부 과정을 거친다.

  • init()은 필요한 Security 객체 생성 후 HttpSecurity에 등록하며
  • configure()은 등록된 Security 객체들을 실제 Filter로 변환하고 FilterChain에 추가하여 FilterChain(*WebSecurity의 build()를 통해 SecurityFilterChain 객체를 FilterChainProxy 객체에 전달)을 최종 생성한다.

위 복습내용을 바탕으로 다시 인증방식에 대해 들어가보자.

formLogin은 HttpSecurity의 formLogin() API를 이용하는 인증방식으로, HttpSecurity에 모여지는 Configurer 중 FormLoginConfigurer를 활용하여 인증방식을 구성하는 방법을 일컫는다.

즉, 말그대로 HttpSecurity 내부의 FormLoginConfigurer를 사용하여 로그인 인증방식을 수행하도록 구성하는 것이며, 이 클래스가 제공해주는 여러 API를 활용한다.

로직을 보면 바로 이해할 수 있는데,

	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                //.formLogin(Customizer.withDefaults());  //client 요청에 대해 기본 인증방식으로 formLogin 방식을 설정
                .formLogin(form -> form
                        .loginPage("/loginPage")
                        .loginProcessingUrl("/loginProc")
                        .defaultSuccessUrl("/", false)
                        .failureUrl("/failed")
                        .usernameParameter("username")
                        .passwordParameter("password")
                        .successHandler((request, response, authentication) -> {
                            System.out.println("authentication success : " + authentication);
                            response.sendRedirect("/home");
                        })
                        .failureHandler((request, response, exception) -> {
                            System.out.println("authentication failure : " + exception);
                            response.sendRedirect("/login");
                        })
                        .permitAll()
                );

        return http.build();
    }

위와 같이 SecurityFilterChain을 생성하기 위해 HttpSecurity의 httpSecurityFormLoginConfigurer라는 configurer를 람다로 전달받고 API를 열거하게 된다.

각 API별로 사용자 지정 프로퍼티 및 객체/요청 정보를 주입하여, 최종적으로 formLoginConfigurer에 해당 내용을 주입, 이 Configurer가 내부적으로 init, configure를 통해 SecurityFilterChain에 해당 흐름을 집어넣는다.

각 API 설정 내용을 살펴보면,

  • .loginPage - 사용자 정의 로그인 페이지로 전환(기본 인증요청에 대한 화면 전환 시 해당 내용을 참고, 기본 url은 ./login이다)
  • .loginProcessingUrl - 인증API를 처리할 경로를 지정한다(기본 url은 ./login이다)
  • defualtSuccessUrl("/", true) - 로그인 성공 후 이동할 페이지를 고정할 수 있으며(true), 기본은 false로 인증이 필요하여 인증페이지로 요청을 발생한 진입점으로 리다이렉트 한다.
  • failureUrl - 인증실패 시 돌아갈 고정 경로를 설정한다.
  • username/password - 인증수행 시 (jsp기준) type id를 지정해주어 해당 매개변수를 전달받아 인증을 수행한다.
  • failureHandler/successHandler - response.sendRedirect("/home");와 같이 인증실패 및 성공 시 후처리를 진행할 공통관심로직을 지정해줄 수 있다. 이때 각각의 인증 후처리를 위한 정보는 Authentication/AuthenticationException에 들어있고 기본값(AuthenticationSuccessHandler/AuthenticationFailureHandler) 을 그대로 사용할 수도 있다.
  • permitAll() - 인증이 필요없는 디폴트 경로에 대해 모든 요청의 접근은 허가한다.

이후 http.build()하여 최종적으로 SecurityBuilder에 해당 내용을 주입, build한다.

참고로 주석처리가 되어있는 부분을 살펴보면

.formLogin(Customizer.withDefaults())

위와 같이 되어있는데, 위 사용자 설정이 아닌 Spring Security의 기본 설정으로 form login을 수행할 수 있긴 하다.

2-1. formLogin 인증흐름

formLogin 인증방식을 사용할 경우, 위에서 알 수 있듯이 요청정보 및 응답을 HttpServletRequest와 HttpServletResponse를 이용하여 흐름을 조절할 수 있다.

만약 요청이 왔을때 인증불가하여 로그인페이지로 이동하는 흐름을 살펴보면,

1) client 요청
2) SecurityFilterChain 작동
2-1) AuthorizationFilter 권한검사 필터 작동
2-2) AccessDeniedException 접근 예외 발생
2-3) ExceptionTranslationFilter 예외 처리 필터 동작
2-4) AuthenticationEntryPoint 인증진입점 이동(인증 시작)
2-5) Configurer API에서 작성한 대로 로그인페이지로 리다이렉트
3) 로그인페이지로 이동
4) username, password 입력 후 인증 성공

이와 같이 이루어진다.

2-2 로그인 인증 필터와 로그인/로그아웃 페이지 생성 필터

UsernamePasswordAuthenticationFilter

이때 중요한 동작 필터 중 하나인 UsernamePasswordAuthenticationFilter는 이름에서 알 수 있듯이, 전달받은 username과 password를 이용하여 인증을 "처리"해주는 formLogin 인증방식의 가장 최후방에서 동작하는 필터이다.

참고로, "인증을 처리하는 필터"의 추상클래스로 AbstractAuthenticationProcessingFilter가 있는데 이 추상클래스를 UsernamePasswordAuthenticationFilter가 상속받아 사용자 id와 비밀번호에 대한 인증처리를 담당한다.

또한 이 filter는 Spring Security의 기본적인 설정 필터이며, 만약 개발자가 복잡한 요구사항을 수행해야 한다면 CustomAuthenticationFilter 클래스를 이용하여 구현하면 된다.

DefaultLoginPageGeneratingFilter / DefaultLogoutPageGeneratingFilter

참고로 위 두 필터는 Spring Security 인증프로세스 초기화 시, Spring Security가 기본적으로 제공해주는 로그인/로그아웃 url 및 해당 페이지를 생성하기 위해 동작하는 필터이다.

formLogin() 인증방식을 사용한다면 위 필터도 자동적으로 동작한다.

2-3. SecurityFilter"Chain" / "Filter"

Filter chain은 말 그대로 filter들이 체인처럼 연결되어있다는 의미이다.

이것은 무엇을 의미하는가?

[HTTP 요청] 
     ↓
Servlet Container
     ↓
DelegatingFilterProxy
     ↓
FilterChainProxy
     ↓
(여러 SecurityFilterChain 중) matches() 결과에 맞는 체인 선택
     ↓
선택된 SecurityFilterChain.getFilters() 리스트 순서대로 실행
     ↓
각 Filter가 요청 처리 (인증/인가 등)
     ↓
문제 없으면 최종 요청은 DispatcherServlet → Controller로 전달

최초 인증요청이 왔을때, SecurityChainProxy이라는 단 1개의 프록시 객체를 찾고, 그 내부에서 요청에 해당하는 SecurityChainFilter를 찾는다. 이 필터체인객체에 등록되어있는 여러 필터들이 동작하고 특정 순서에 UsernamePasswordAuthenticationFilter가 동작할 것이다.

이때 세부적으로,

FilterChain이 본인에 해당하는 요청을 matches로 활성화여부를 판단했다면,
내부적으로 존재하는 filter에서 requestMatcher를 통해 또한 필터의 활성화여부를 또 판단하게 된다.

2-4. 최종 인증처리

여기까지 이해했다면 전체적인 formLogin 인증처리의 구조와 체계를 이해한 것이다.

이후 UsernamePasswordAuthenticationFilter는 username, password를 UsernamePasswordAuthenticationToken에 저장한다.

이 token 정보를 활용하여 AuthenticationManager를 호출하여 인증처리를 최종 진행하게 되며, AuthenticationManager는 DB에 저장된 계정정보를 읽어와서 인증정보의 일치 여부를 확인하게 된다.

인증 성공 시

사용자 정보 및 권한 정보가 usernamePasswordAuthenticationToken에 다시 한번 더 저장된다.

이후 세션(HttpSession) 관련 작업을 진행하여, 로그인을 유지할 수 있는 세션 작업을 수행한다.

  • 로그인 성공 후 SessionAuthenticationStrategy 클래스를 호출하여 관련 작업을 수행한다.
  • 인증객체(Authentication)을 SecurityContext(Spring이 저장하는 인증정보)에 저장하고, session에 이 securityContext를 저장하기 위해 SecurityCOntextHolder를 호출하여 이 작업을 수행한다.

로그인 성공 후 해당 로그인 정보를 기억할 수도 있는데, Remember-me 설정이 활성화 되었을 경우 RememberMeServices.loginSuccess를 호출하여 로그인 정보를 기억한다.

인증 성공 이벤트를 게시(ApplicationEventPublisher), 인증성공에 대한 핸들러를 호출한다(AuthenticationSuccessHandler).

인증 실패 시

SecurityContextHolder는 인증객체정보인 SecurityContext 정보를 아예 삭제하며, 로그인 기억 정보를 하지 않고 실패응답을 전달한다(RememberMeServices.loginFail).

인증 실패에 대한 핸들러를 호출한다(AuthenticationFailureHandler).

[사용자 요청: id / pw 입력]
            │
            ▼
UsernamePasswordAuthenticationFilter
            │
            │─> username, password를 담아
            │    UsernamePasswordAuthenticationToken 생성
            ▼
AuthenticationManager
            │
            │─> DB(UserDetailsService 등)에서 계정 정보 조회
            │─> 입력 정보와 비교
            ▼
 ┌───────────────┐
 │ 인증 성공 시   │
 └───────────────┘
            │
            │─> Authentication 객체에 사용자 정보 & 권한 저장
            │
            │─> SessionAuthenticationStrategy 호출
            │       (세션 관리, 로그인 유지)
            │
            │─> SecurityContext에 Authentication 저장
            │       → SecurityContextHolder 호출
            │
            │─> RememberMeServices.loginSuccess 호출
            │       (Remember-me 활성화 시)
            │
            │─> 인증 성공 이벤트 게시 (ApplicationEventPublisher)
            │
            │─> AuthenticationSuccessHandler 호출
            ▼
       사용자 요청 처리 완료
       
 ┌───────────────┐
 │ 인증 실패 시   │
 └───────────────┘
            │
            │─> SecurityContextHolder에서 SecurityContext 삭제
            │
            │─> RememberMeServices.loginFail 호출
            │
            │─> Authentication

이에 대해 간단히 정리하자면 위와 같이 도식화할 수 있고, 상기의 내용이 formLogin 인증처리 방식에 대한 내용이다.

3. httpBasic 인증방식(httpBasic())

0개의 댓글