Skip to content

스프링 시큐리티(Spring Security) 아키텍처 이해하고 커스텀 필터 구현하는 법 ​

개인 프로젝트하면서 항상 인증/인가 단계에서 다시금 해매고 문서를 찾아보게 되어 한번 정리할 겸 문서 작성해봅니다.

제공해 주는 기능 ​

  • 어플리케이션 사용 인증 및 권한
  • FormLogin 활성화 시 기본 로그인 화면
  • 기본 유저 생성
  • 비밀번호 암호화 (Bcrypt) 및 암호 저장소 제공
  • 로그아웃
  • CSRF 공격 방지
  • 세션 탈취 방지
  • ETC...

스프링 시큐리티를 이해하기 위해서는 다음을 기본적으로 이해하고 있어야한다.

  • Servlet
  • Context Hierarchy
  • Servlet Container
  • DispatcherServlet
  • FilterChain

Servlet ​

자바 프로그램 (함수, 클래스 등...)

DispatcherServlet ​

많은 서블렛을 연결해주는 중앙의 서블렛이다.

DispatcherServlet은 다른 서블렛과 마찬가지로 같은 서블레이여서, 정의되고 설정이나 web.xml에 매핑되어 있어야한다.

DispatcherServlet은 스프링 설정에 따라 요청매핑, 뷰 만들기, 예외처리 등에 사용될 위임 컴포넌트를 찾습니다.

다음 예시는 서블렛 컨테이너에 의해 자동으로 Dispatcher가 설정되고 등록되고 생성되는 과정을 보여줍니다.

kotlin
class MyWebApplicationInitializer : WebApplicationInitializer {

    override fun onStartup(servletContext: ServletContext) {

        // Load Spring web application configuration
        val context = AnnotationConfigWebApplicationContext()
        context.register(AppConfig::class.java)

        // Create and register the DispatcherServlet
        val servlet = DispatcherServlet(context)
        val registration = servletContext.addServlet("app", servlet)
        registration.setLoadOnStartup(1)
        registration.addMapping("/app/*")
    }
}

Dispatcher가 제공해 주는 몇가지 기능과 기본 구현체들에 대해 적고 지나가보자.

  • HandlerMapping
    • BeanNameUrlHandlerMapping
    • RequestMappingHandlerMapping
  • HandlerAdapter
    • HttpRequestHandlerAdapter
      • RequestMappingHandlerAdapter
    • SimpleControllerHandlerAdapter
  • HandlerExceptionResolver
    • ExceptionHandlerExceptionResolver
    • ResponseStatusExceptionResolver
    • DefaultHandlerExceptionResolver
  • ViewResolver
    • InternalResourceViewResolver
  • RequestToViewNameTranslator
    • DefaultRequestToViewNameTranslator
  • MultipartResolver
    • None(없음)
  • LocaleResolver
    • AcceptHeaderLocaleResolver

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.servlet.spring-mvc 스프링 프레임워크에서는 이렇게 지원하지만, 스프링 부트에서는 자체적으로 내장된 서블릿 컨테이너에 자동으로 후킹됩니다.

정의된 ServletFilter와 서블릿가 알아서 자동으로 매핑됩니다. (RequestMapping 등...) https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.servlet.embedded-container.servlets-filters-listeners.beans

Servlet이나 Filter, *Listener 서블릿들이 빈으로 등록되면 알아서 서블렛 컨테이너에 등록됩니다.

Servlet Container ​

들어온 HTTP 요청에 대해서 HttpRequest 쓰레드를 생성해주고 서블렛에 매핑해준다.

Filter ​

필터

서블렛 인증 관련 용어 및 구조 설명 ​

SecurityContextHolder ​

Spring Security에 의해 누가 인증됐는지를 저장하는 공간입니다.

SecurityContext ​

SecurityContextHolder로 얻을수 있습니다.그리고 여기엔 현재 인증된 유저의 정보가 저장되어 있습니다.

Authentication ​

유저가 인증정보를 SecurityContext에 저장되고 AuthenticationManager에 의해 입력 또는 생성되는 객체입니다.

GrantedAuthority ​

Authentication에 있는 유저에게 부여된 권한 정보가 들어있는 객체입니다.

AuthenticationManager ​

Spring Security에 등록된 필터들이 어떻게 인증 단계를 동작해야하는지 정의되어있습니다.

ProviderManager ​

가장 흔히 사용되는 AuthenticationManger의 구현체이다.

AuthenticationProvider ​

ProviderManager가 특정 방식의 인증을 수행할때 주로 사용된다. ex. JwtAuthenticationProvider, DaoAuthenticationProvider 등..

Request Credentials with AuthenticationEntryPoint ​

클라이언트에게 인증정보를 요구할때 사용된다. (Login page로 리디렉트 또는 401 응답 등...)

AbstractAuthenticationProcessingFilter ​

인증을 위한 기본 필터. 이것은 높은 수준의 인증 흐름과 조각이 함께 작동하는 방식에 대한 좋은 아이디어를 제공합니다.

AuthenticationProvider를 구현해 인증 하기 ​

CustomAuthenticationProvider 구현 ​

AbstractAuthenticationProcessingFilter가 Authentication 객체를 만들어주면 AuthenticationManager가 받아 인증을 한다.

이때 AuthenticationManager는 Provider를 갖고 인증을 하므로 우리가 그 부분을 구현하는거다.

java
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
        final String name = authentication.getName();
        final String password = authentication.getCredentials().toString();
        if (!"admin".equals(name) || !"system".equals(password)) {
            return null;
        }
        return authenticateAgainstThirdPartyAndGetAuthentication(name, password);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
    
    private static UsernamePasswordAuthenticationToken authenticateAgainstThirdPartyAndGetAuthentication(String name, String password) {
      final List<GrantedAuthority> grantedAuths = new ArrayList<>();
      grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
      final UserDetails principal = new User(name, password, grantedAuths);
      return new UsernamePasswordAuthenticationToken(principal, password, grantedAuths);
    }
}

Security Manager에 등록 ​

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomAuthenticationProvider authProvider;

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = 
            http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.authenticationProvider(authProvider);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(request -> request.anyRequest()
                .authenticated())
            .httpBasic(Customizer.withDefaults())
            .build();
    }
}

OncePerRequestFilter로 구현 ​

kotlin
    addFilterBefore<UsernamePasswordAuthenticationFilter>(
        JwtTokenFilter(jwtTokenService, userService)
    )


SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(
          subject,
        credential,
        listOf(
            SimpleGrantedAuthority(role)
        )
    )

참고 링크 ​

// todo

Email: echo.youn@kakao.com