1 분 소요

스프링 시큐리티

필요성

애플리케이션을 개발함에 있어서 보안은 굉장히 중요한 이슈이다. 보안이 뚫리게 되면 단순히 데이터, 돈을 잃는 것을 넘어서 수년간 쌓아온 브랜드 평판과 유저로부터의 신뢰를 한번에 잃을 수 있다. 그렇다고 애플리케이션을 만들때마다 자체 보안 시스템을 만드는 것은 매우 비효율적이다. 기능적으로 문제가 발생할 위험도 있고 많은 시간과 돈이 들어간다. 스프링은 이런 문제를 해결해주기 위해 스프링 시큐리티라는 스프링 하부 프레임워크를 제공한다.

흐름

img1

위 그림은 가장 기본적인 Security의 흐름을 보여준다.

  1. 클라이언트로부터 HTTP 요청이 들어오면 Spring Security의 필터가 이를 가로챈다.

  2. 요청에 들어있는 유저의 아이디, 패스워드를 추출하여 Authentication을 구현한 UsernamePasswordAuthenticationToken을 생성하고 AuthenticationManager를 구현한 ProviderManager의 authenticate()메서드를 호출하며 이를 넘겨준다.
  3. ProviderManager는 모든 AuthenticationProvider를 순차적으로 호출하여 인증을 수행한다.
  4. AuthenticationProvider에서는 UserDetailService/UserDetailManager와 PasswordEncoder의 도움을 받아 인증을 수행하며 Authentication 객체를 반환한다.
  5. 인증에 성공하게 되면 AuthenticationManager는 Authentication 객체를 반환하고 필터에서 이를 받아 SecurityContextHolder 내부의 Context에 인증 객체를 저장하게 된다.
  6. 만약 실패하게 되면 AuthenticationManager는 또 다른 AuthenticationProvider를 호출하여 위 과정을 계속 반복하게 된다.

이제 각각의 역할에 대해서 더 자세하게 알아보자

Spring Security Filter

DefaultLoginPageGeneratorFilter

스프링 시큐리티 의존성을 추가하고 우리의 서비스에 접근하려고 하면 아래와 같은 로그인 페이지로 넘어가지는 경험을 해봤을 것이다.

img1

이러한 로그인 기능을 구현한 필터가 바로 DefaultLoginPageGeneratorFilter이다.

img1

요청으로부터 로그인 정보를 확인하여 로그인을 하지 않은 상태라면 위와 같은 로그인 페이지를 만든다. 실질적으로 로그인 페이지를 만드는 로직은 generateLoginPageHtml()메서드 안에 존재한다.

img1

한땀한땀 html 코드가 작성되어 있는 것을 볼 수 있다.

UsernamePasswordAuthenticationFilter

img1

다음으로는 UsernamePasswordAuthenticationFilter이다. 코드를 보면 request로부터 유저의 아이디, 패스워드를 추출하고 Authentication을 구현한 UsernamePasswordAuthenticationToken을 생성하는 것을 볼 수 있다. 그리고 AuthenticationManager의 authenticate() 메서드를 호출하며 이를 넘기는 것을 볼 수 있다. 그렇다면 이 UsernamePasswordAuthenToken의 정체는 무엇일까?

아래 코드를 보자.

Authentication

img1img1

Authentication 객체는 현재 요청하고 있는 주체의 정보가 들어있다. 이 객체는 SecurityContextHolder 안의 ContextHolder에 실질적으로 저장되는 유저의 정보이기도 하다. 뜬금없이 Authentication에 대해서 설명한 이유는 UsernamePasswordAuthenticationToken이 authentication을 구현한 클래스이기 때문이다. (실제로는 AbstractAuthenticationToken을 상속받는데 이 추상 클래스가 Authentication을 구현하고 있다.)

UsernamePasswordAuthenticationToken

img1

UsernamePasswordAuthenticationToken을 보면 두 개의 생성자가 있는 것을 볼 수 있다. 위 생성자는 내부에서 setAuthenticated(false)로 인증 실패 상태를 넣는 것을 볼 수 있고 아래는 true를 넣어 성공 상태를 넣는다. 우리가 위에서 설명한 UsernamePasswordAuthenticationFilter 내부에서는 첫번째 생성자를 이용하여 객체를 생성한다.

그렇다면 두번째 생성자는 언제 사용될까? AuthenticationProvider에서 실질적인 인증을 수행한 후 인증에 성공하면 Authentication 객체를 반환하는데 이 때 두번째 생성자를 사용하게 된다.

다음으로 AuthenticationManager에 대해서 알아보자.

AuthenticationManager

AuthenticationManager는 인터페이스이고 단 하나의 메소드 authenticate()를 정의하고 있다. AuthenticationManager를 구현한 ProviderManager내부의 authenticate() 메서드 내부를 보자.

img1

코드를 보면 알 수 있듯이 반복문을 활용하여 가지고 있는 모든 Provider를 활용하여 인증을 시도한다. 모든 Provider에 대해서 인증이 실패하면 실패 Authentication 객체를 반환하고 성공하