본문 바로가기

Spring & Spring Boot

Spring Security 구조와 동작 원리

Spring Security


 Spring Security란, Http Request가 DispatcherServlet으로 도달하기 전 Servlet의 Filter를 기반으로 Request에 대한 인증과 인가를 적용시켜주는 프레임워크이다.

 

 일반적으로 클라이언트에서 서버로 요청을 보내면, DispatcherServlet이 하나의 HttpServeletRequest를 받아서 요청을 처리하고 HttpServletResponse 응답을 클라이언트로 보낸다. 그런데, 하나 이상의 Filter가 포함된다면, 클라이언트에서 보낸 요청이 Servlet으로 전달되기 전에 Filter를 거치게 된다.

 

 클라이언트가 애플리케이션에 하나의 요청을 보내면, 컨테이너는 하나의 FilterChain을 생성한다. FilterChain 내부의 Filter는 말 그대로 '필터'의 역할을 하게 된다. 클라이언트에서 보낸 요청이 다음 FilterServlet에 전달되지 않도록 걸러낼 수 있다.

 

 GET http://localhost:8080/memo/123 을 한다고 생각해보자. 이 API는 인가(권한 체크)를 필요로 한다. 해당 memo의 작성자가 아니면 볼 수 없어야 하기 떄문이다.

이런 종류의 인증/인가 기능을 Request가 우리 서버(Servlet)에 도착하기 전에 미리 수행해주는(걸러 주는) 역할을 하는 게 Filter이다.

 

+이러한 Filter들이 묶여 있는 것을 SecurityFilterChain이라고 한다.

 

 

 사용자의 요청이 Servlet에 전달되어 자원에 접근하기 전에, Spring Security는 Filter의 생명주기를 이용해서 인증과 권한 작업을 수행한다. 

 Servlet 컨테이너는 주로 웹 애플리케이션의 생명주기와 Servlet 객체의 라이프사이클을 관리하지만, 스프링 컨테이너는 Bean들의 라이프사이클을 관리하며, 객체 생성, 의존성 주입, 초기화, 소멸 등의 기능을 한다. 이 때문에 Servlet 컨테이너에서는 스프링 컨테이너에 등록된 Bean을 인식할 수 없다.

 따라서 스프링 시큐리티에서는 DelegatingFilterProxy라는 Servlet Filter의 구현체를 제공한다. DelegatingFilterProxy는 Servlet 매커니즘을 통해 서블릿의 필터로 등록될 수 있으며 스프링에 등록된 Bean을 가져와 의존성을 주입할 수 있다. 이렇게 DelegatingFilterProxy는 Servlet 컨테이너의 생명주기와 스프링의 ApplicationContext(스프링 프레임워크에서 제공하는 핵심 컨테이너로서, 빈(Bean)들을 생성, 관리, 검색하고, 빈 간의 의존성을 주입하는 역할을 수행) 사이를 연결하는 다리 역할을 하게 된다. DelegatingFilterProxy에서는 Servlet 필터에서 받은 요청과 응답을 스프링 Bean으로 등록된 필터로 전달한다(CorsFilter, JwtFilter 등).

 

 

 

 

 

 

 

 

 

 

 

 

 

 DelegatingFilterProxy의 내부에는 FilterChainProxy라는 것이 있는데, 이것도 Spring Security에서 제공하는 Filter이다.

 

 

 DelegatingFilterProxy의 내부에는 FilterChainProxy라는 것이 있는데, 이것도 Spring Security에서 제공하는 Filter이다.

 FilterChainProxy는 DelegatingFilterProxy를 통해 받은 요청과 응답을 SecurityFilterChain에 전달하고 작업을 위임하는 역할을 한다.

 SecurityFilterChain은 인증을 처리하는 여러 개의 SecurityFilter를 담는 FilterChain이다. 또, FilterChainProxy를 통해 Servlet Filter와 연결되고 FilterChainProxy에서 요청에 대해 호출할 SecurityFilter를 결정한다.

 SecurityFilterChain에 담긴 SecurityFilter들이 Filter로 동작하며 요청이 걸러지게 되는 것이다.

 DelegatingFilterProxy에서 바로 SecurityFilterChain을 실행시킬 수도 있지만, 중간에 FilterChainProxy를 두는 이유는 바로 서블릿을 지원하는 시작점 역할을 하기 위해서다.

 만약 서블릿에서 문제가 발생한다면 FilterChainProxy의 문제라는 것을 바로 알 수 있다.

 

 

 

 

 

Spring Security 인증 아키텍쳐


 

 

 UsernamePasswordAuthenticationToken 은 Authentication 인터페이스의 구현체인 AbstractAuthenticationToken 의 자식 클래스임(이 관계에 신경써야 함)

기본적인 흐름은 다음과 같다. 

1. Http Request
2AuthenticationFilter에서 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에 전달. 여기서 UsernamePasswordAuthenticationToken 은 로그인 시 전달받은 userIdpassword를 통해 생성
3AuthenticationManager은 등록된 AuthenticationProvider들을 조회하여 인증 요구
4AuthenticationProvider UserDetailService를 통해 입력받은 아이디에 대한 사용자 정보를 User DB(우리가 구현한 DB) 에서 조회
5. User DB 에 로그인 요청한 사용자의 정보가 있는 경우 UserDetails로 꺼냄
6. 인증이 성공된 Authentication을 생성하여 AuthenticationManager로 넘겨줌
7. AuthenticationManager은 Authentication을 AuthenticationFilter로 전달
8AuthenticationFilter은 전달받은 Authentication을 Spring security 인메모리 세션저장소인 SecurityContextHolder에 저장

 

 

 

 

위에서 언급한 클래스, 인터페이스를 알아보자.

 

1. UsernamePasswordAuthenticationToken


//UsernamePasswordAuthenticationToken의 코드 중 일부(생성자)
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true);
}

 

  • Authentication을 구현한 AbstractAuthenticationToken의 하위 클래스이다.
  • principalusername(userId)
  • credentialspassword

 

 

2. AuthenticationManager


  • Authentication 을 만들고 인증을 처리하는 interface
  • 로그인시 인자로 받은 AuthenticationAuthenticationProvider를 통해 유효한지 처리하여 Authentication 객체를 리턴한다.

 

 

 

3. AuthenticationProvider


//AuthenticationManagerBuilder 에 List<>로 존재
public class AuthenticationManagerBuilder extends AbstractConfiguredSecurityBuilder<AuthenticationManager, AuthenticationManagerBuilder> implements ProviderManagerBuilder<AuthenticationManagerBuilder> {
    .....
    private AuthenticationManager parentAuthenticationManager;
    private List<AuthenticationProvider> authenticationProviders = new ArrayList();
		.....
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

 

  • 실제 인증을 담당하는 인터페이스
  • 인증 전 Authentication 객체를 받아서 DB 에 있는 사용자 정보를 비교하고 인증된 객체를 반환

 

 

 

4. UserDetailsService


//UserDetailsService의 코드
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • DB 에서 유저 정보를 가져오는 역할
  • loadUserByUsername() 메소드를 통해서 DB 에서 유저 정보를 가져온다.
  • 커스텀하게 사용하고 싶다면 해당 interface 를 implements 받아서 loadUserByUsername() 메소드를 구현하면 됨

 

 

 

 

5. UserDetails


//UserDetails의 코드
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

 

  • 사용자의 정보를 담는 인터페이스
  • 기본 오버라이드 메소드는 다음과 같음
메소드 설명
getAuthorities() 계정의 권한 목록을 리턴
getPassword() 계정의 비밀번호 리턴
getUsername() 계정의 고유한 값 리턴
isAccountNonExpired() 계정의 만료 여부 리턴
isAccountNonLocked() 계정의 잠김 여부 리턴
isCredentialsNonExpired() 비밀번호 만료 여부 리턴
isEnabled() 계정의 활성화 여부 리턴

 

 

 

 

6. SecurityContextHolder, SecutiryContext, Authentication


  1. SecurityContextHolder
    • SecurityContext 를 현재 스레드와 연결 시켜주는 역할
    • 스프링 시큐리티는 같은 스레드 의 어플리케이션 내 어디서든 SecurityContextHolder 의 인증 정보를 확인 가능하도록 구현되어 있는데 이 개념을 ThreadLocal 이라고 함.
  2. SecutiryContext
    • Authentication 의 정보를 가지고 있는 interface
    • SecurityContextHolder.getContext() 를 통해 얻을 수 있음
  3. Authentication
    • 현재 접근하는 주체의 정보와 권한을 담는 인터페이스
    • AuthenticationManager.authenticate(Authentication) 에 의해 인증된 principal 혹은 token
    • 다음은 Authentication의 정보들임

    이름 설명
    Principal 사용자 정보(UserDetails가 들어갈 것임)
    authorities 사용자에게 부여된 권한EX) ROLE_ADMIN
    credentials 자격 증명