🎯 목표

Spring Security 를 이해하기 위한 큰 양대 산맥이있는데,
그중 하나가 인증(Authentication) 이고 다른 하나는 인가(Authorization) 입니다.

이번 글에서는 인증과 인가의 의미를 간단히 알아보고
인증과 관련된 내부 프로세스를 알아보도록 하겠습니다.
인가 프로세스는 추후에 따로 다뤄보겠습니다.



🍞 인증과 인가

짧게 요약하자면 다음과 같습니다.

인증은 어떤 시스템에 소속된 사용자인지 아닌지를 판단하는 것이고,
인가는 시스템의 자원을 사용하기 위한 권한이 있는지를 판단하는 것입니다.


좀 더 길게 얘기하자면 아래와 같습니다.

예를 들어 여러분이 어떤 회사에 소속된 사람이라고 해보겠습니다.
아침에 출근을 해서 회사의 정문 앞까지 왔습니다.

그 다음은 뭘까요? 아마 여러분들은 회사에서 준 카드키 같을 걸 통해서
정문을 통과하게 됩니다. 또는 지문 인식을 하는 곳이면 지문을 찍어서 들어가겠죠.

이처럼 자신이 어떤 회사(=시스템)에 소속된 일원인지 아닌지를 판단하는 것이
바로 인증(Authentication) 입니다.


그러면 인가는 뭘까요?

이번에는 여러분들이 막 입사한 신입사원이라고 생각해보죠.
다행히 회사에서 준 카드키를 통해서 무사히 정문을 통과해서
여러분은 인증 과정을 거쳤습니다.

그런데 신입사원분이 자신의 배정받은 자리가 아니라,
회사 대표님의 자리에 가서 컴퓨터를 사용해도 될까요?

당연히 안됩니다.

이처럼 신입사원분은 ...

  • 자신이 배정받은 자리에 있는 컴퓨터, 필기류, 책상, 의자 등의 자원
  • 회사의 모든 사람들이 사용할 수 있는 공용재(ex: 화장실, 프린터 등등)

... 를 사용할 권한이 있지만 다른 사람들의 자원을 마구잡이로 사용할 수 없습니다.

이렇듯 회사(=시스템) 내에서 자신의 직급(=권한)에 따라
자원의 사용 가능 여부를 판단하는 것이 바로 인가(Authorization) 입니다.


보충 : 인증과 인가는 칼 같이 나뉘는가? NO!

인가는 인증을 포괄하는 개념입니다.

신입사원이 애초에 배정받은 자리도 결국은 인증을 통해서
회사 정문을 통과해야 사용할 수 있는 거잖아요?

이렇듯 한 회사의 일원임을 증명하는 것 자체가 회사 내의 어떤 자원을
사용하기 위한 권한으로 따질 수도 있는 것이죠.
그래서 인증은 인가에서 말하는 권한 중에서 가장 최소한 권한이기도 합니다.




🍞 인증 프로세스 따라잡기

그렇다면 일반적으로 저희가 인터넷에서 볼 수 있는 인증의 형태가 뭘까요?
바로 로그인(Login)입니다.

저희가 어떤 사이트의 자원을 사용하기 위해서는 가장 첫 관문이죠.

스프링 시큐리티도 이러한 로그인 기능을 제공하는데,
이를 위한 간단한 Spring 설정 클래스를 작성해봅시다.


보안 설정 클래스 작성

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests(auth -> {
				auth.requestMatchers(PathRequest.toStaticResources()
                						.atCommonLocations()).permitAll();

                // 1. 모든 자원은 인증된 계정으로만 접근이 가능하다.
                auth.anyRequest().authenticated();
			})

            // 2. 로그인 기능 활성화, 단 세부 설정은 security 기본 설정 사용
			.formLogin(Customizer.withDefaults());
		return http.build();
	}

    // 임시 사용자 생성 ( id : user / pw: 1111 )
	@Bean
	public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
		UserDetails user = User.withUsername("user")
        						.password("{noop}1111")
                                .roles("USER").build();
		return new InMemoryUserDetailsManager(user, db, admin);
	}
}
  • auth.anyRequest().authenticated(); : 모든 요청은 인증 사용자만 가능토록 함.
  • formLogin(Customizer.withDefaults()); : 로그인 설정을 활성화.
  • InMemoryUserDetailsManager 를 통해서 임시 계정 생성:
    • id: user
    • pw: 1111



보안 필터 목록 미리보기

지금부터 프로세스를 추적할 건데
그전에 먼저 SecurityFilterChain 에 적용된 보안 필터 중 3가지 필터의
이름과 역할을 알고 넘어가겠습니다. 필터들의 목록은 아래 그림과 같습니다.

여러 필터가 있지만 우리가 이번에 보게 될 필터는 3가지 입니다.

  1. UsernamePasswordAuthenticationFilter
    AbstractAuthenticationProcessingFilter 를 Extend 한 클래스이며,
    Form 로그인과 관련된 인증 프로세스를 총괄하는 Filter 입니다.
  1. AuthorizationFilter
    자원에 대한 권한을 검사하고 권한이 없다면 예외를 발생시키는 필터입니다.

  2. ExceptionTranslationFilter
    AuthorizationFilter 가 뱉어낸 Security 예외를 Catch 해서 이를 분석합니다.
    그러고 나서 예외의 종류에 따라 적절한 프로세스를 태웁니다.



인증되지 않은 상태로 자원접근

이 목차는 보안 필터 목록 미리보기 에서 본 필터 중
AuthorizationFilter, ExceptionTranslationFilter 가 일으키는 프로세스 입니다.

프로젝트를 실행시키고
먼저 로그인을 하지 않은 상태에서 http://localhost:8080 에 접속해봅시다.
그러면 spring security 내부적으로 아래와 같은 일들이 발생합니다.


AutorizationFilter 가 해당 요청이 인증된 사용자가 보낸 것인지 아닌지를 체크합니다.
아닌 것을 감지하면 예외를 던집니다.


이후에 ExceptionTranslationFilter 가 예외를 잡아내고 분석하여
적절한 프로세스를 태웁니다.

지금은 sendStartAuthentication 이라는 메소들 호출하는 것을 확인할 수 있습니다.
여기서 AuthenticationEntryPoint.commence 메소드 호출이 핵심입니다.


LoginUrlAuthenticationEntryPoint.commence 메소드가 호출되고 redirect 가 되도록 response 에 설정을 합니다. redirect 경로는 에 의해서 /login 입니다.


결과적으로 spring security 의 form 로그인 기본 설정에 의해 생성되는
로그인 기본 페이지가 화면에 보입니다.



로그인 처리 (Flow Chart)

이 목차는 보안 필터 목록 미리보기 에서 본 필터 중
UsernamePasswordAuthenticationFilter 태우는 프로세스 입니다.

로그인 페이지에서 username: user, password: 1111 을 입력하고
Sign in 버튼을 클릭하면 어떤 일이 일어날까요?
내부적으로 일어나는 현상을 먼저 Flow Chart 로 알아봅시다.

참고로 AbstractAuthenticationProcessingFilter
UsernamePasswordAuthenticationFilter 의 부모 클래스입니다.

  • AbstractAuthenticationProcessingFilter 가 현재 요청이 로그인 처리 요청인지 확인합니다.
  • 로그인 처리 요청이 맞다면 파라미터에서 username 과 password 를 추출하
    Authentication Token(정확히는 UsernamePasswordAuthenticationToken) 를
    생성한 후 AuthenticationManager 에게 Token 을 주면서 인증 처리를 위임합니다.
  • AuthenticationManager 는 자신이 갖고 있는 List<AuthenticationProvider> 목록 중에서 전달 받은 Token 을 처리할 수 있는 AuthenticationProvider 에게 인증 작업을 다시 위임합니다.
    • 처리 가능 여부는 AuthenticationProvider 의 support 메소드를 통해서 알아냅니다.
    • 만약 어떠한 Provider 도 처리를 안해주면 예외를 던집니다.
    • 저희가 현재 하는 로그인 처리는 UsernamePasswordAuthenticationToken 이라는 Token 을 사용하고 이를 처리해주는 게 DaoAuthenticationProvider 인스턴스입니다.
  • DauAuthenticationProvider 내에서는 ...
    • UserDetailsService 인스턴스를 통해서 사용자 정보를 조회하고,
    • 사용자 정보가 조회되지 않거나, 비밀번호가 알맞지 않는 등의 문제가 생기면
      AuthenticationException 타입의 예외를 던지고
    • 정상적으로 처리되면 사용자 정보가 담긴 AuthenticationToken 를 새로 생성해서 반환

최종적으로 AbstractAuthenticationProcessingFilter 는...
인증 성공: AuthenticationProvider 가 생성한 AuthenticationToken 을 받아냅니다.
인증 실패: AuthenticationException 타입의 예외를 Catch 합니다.

이렇게 각각 성공 실패에 따른 작업은 아래와 같습니다.

그림출처: https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-authenticationentrypoint

성공 또는 실패를 하면 어떤 일이 일어나는 지는 아래와 같이 공식문서에서 설명합니다.

워낙 방대한 내용이니 다 알아볼 수는 없습니다.
대신 성공했을 때는 AuthenticationSuccessHandler,
실패했을 때는 AuthenticationFailureHandler 를 사용해서 최종적인 처리를 한다는
것만 눈여겨 보시기 바랍니다.




로그인 처리 (Source Code)

자 이번에는 코드를 통해서 위에서 말한 과정들이 정말 이루어지는지 확인해보죠.
로그인 페이지에서 username: user, password: 1111 을 입력하고
Sign in 버튼을 클릭해서 로그인을 실행해봅시다.


  1. 먼저 AbstractAuthenticaionProcessingFilter 에서 현재 요청이 로그인 처리를 위한 요청인지 확인합니다.

  2. 하위 클래스가 구현한 attemptAuthentication 메소드를 호출합니다.


  1. 먼저 Form 요청을 통해서 들어온 username, password 파라미터의 값을 읽고
    이 정보를 Authentication Token 으로 감쌉니다.

  2. 해당 Token 을 AuthenticationManager 에게 전달하면서 인증 처리를 위임합니다.


  1. AuthenticationManager (구현체 이름은 ProviderManager) 는 자신이 갖고 있는 List<AuthenticationProvider> 를 순회하면서 현재 Token 에 대한 인증 처리가 가능한 것을 찾아냅니다.

  2. 토큰 처리가 가능한 AuthenticationProvider 가 발견되면 해당 Provider 에게 다시 한번 인증 처리를 위임합니다. (현재는 DaoAutnenticationProvider 입니다)


DaoAuthentication 의 부모 클래스인 AbstractUserDetailsAuthenticationProvider 클래스의 authenticate 메소드가 호출되고, 전달받은 AuthenticationToken 의 정보를 통해서 사용자의 정보를 조회합니다.


DaoAuthenticationProvider.retreiveUser 메소드가 호출되고,
이때 인스턴스가 참조하는 UserDetailsService 인스턴스를 통해서 사용자의 id(=username)과 일치하는 아이디가 있는지를 조회합니다.

만약 못찾으면 UsernameNotFoundException 예외를 던집니다.


이후에 조회된 사용자 정보에서 비밀번호 정보를 추출하여,
client 에게서 전달받은 비밀번호와 matching 이 되는지 확인합니다.

매칭되지 않으면 BadCredentialsException 을 던집니다.


사용자 정보 조회, 비밀번호 매칭을 해서 아무 문제가 없었다면
Authentication 타입 인스턴스(=Authentication Token) 을 생성하여
AuthenticationManager 에게 반환합니다.

또한 AuthenticationManager 는 이 반환값을 그대로
AbstractAuthenticationProcessingFilter 에게 반환하게 됩니다.


  1. (인증 처리가 문제없이 처리가 됐다면) AuthenticationManager 에게 인증 처리 위임하여 최종적으로 Authentication Token 을 반환 받습니다.

  2. 이어서 successfulAuthentication 메소드를 호출하여 최종적인 처리를 수행합니다.

  3. 반대로 인증 처리에 문제가 발생하여 UsernameNotFoundException, BadCredentialsException 와 같은 예외가 발생하면 여기서 Catch 되고 unsuccessfuleAuthentication 메소드를 통해 최종 실패 처리를 합니다.

참고:
UsernameNotFoundException, BadCredentialsException 모두 AuthenticationException 의 Extend 클래스입니다.


만약 인증 성공을 해서 successfulAuthentication 를 호출했다면...

마지막으로 SuccessHandler 에 의해 최종 동작을 결정합니다.
아무 설정을 안했다면 SavedRequestAwareAuthenticationSuccessHandler 가 동작하고,
이 핸들러는 인증되지 않은 사용자여서 로그인 페이지로 redirect 하기 전에
사용자가 최종적으로 요청했던 url 을 기억했다가, 로그인이 성공하면
해당 url 로 요청을 redirect 시켜주는 핸들러입니다.


반대로 인증에 실패해서 unsuccessfuleAuthentication 메소드가 호출됐다면...

마지막으로 FailureHandler 에 의해서 최종 동작을 결정합니다.
기본적으로 SimpleUrlAuthenticationFailureHandler 가 동작하여,
로그인 페이지로 다시 redirect 됩니다.



이상으로 인증 프로세스에 대한 전반적은 과정을 추적해봤습니다.
Spring Security 를 처음 접하신 분들도 저처럼 디버깅 포인트를 잡으면서
공부하면 많이 도움이 되실 겁니다.

이상으로 글을 마치겠습니다.

profile
백엔드 개발자로 일하고 있는 식빵(🍞)입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN