본문 바로가기
감자-갱생-프로젝트

Spring Security는 사드세요... 제발 - Architecture 편

by kriorsen 2025. 4. 14.

날로 발전하는 지피티의 알랑방구

안녕하세요, ChatGPT 공증 끝판왕입니다.

 

📌  Spring Security란


Spring Security는 인증, 인가, 취약점 보호를 제공하는 프레임워크이다.

 

프로젝트에 Spring Security를 도입하기 위해서는 두 가지 준비물이 필요하다:

1. 악명 높은 러닝커브를 얕보기 위한 근거 없는 자신감

2. 나 대신 오류를 해결해 줄 지피티

 

 

📌  근자감이 쏘아 올린 큰 공


원하는 초능력을 가질 수 있지만 첫 댓글이 부작용을 정한다

실행 흐름만 알면 프레임워크를 마스터할 수 있다
 ㄴ 평생 Spring Security의 실행 흐름을 파악할 수 없음

 

 

동영상을 보고, 도식화된 이미지를 서치하고, 블로그 글들을 읽어도 머리에 하나도 안 들어와서 결국 공식 문서를 찾아보기로 했다.

 

🔠  영어 울렁증 극복기


영어 공부도 하고 Spring Security도 배우고 완전 럭키비키잖아 🍀

 

📌  Servlet Filter

Spring Security는 Servlet Filter를 기반으로 동작하므로, 먼저 Filter에 대해 알아야 한다. 클라이언트가 애플리케이션에 요청을 보내면, 서블릿 컨테이너는 요청 경로에 따라 해당 요청을 처리해야 하는 Filter 인스턴스들과 Servlet을 포함하는 FilterChain을 다음과 같이 생성한다.

 

하나의 요청 당 최대 하나의 Servlet만이 처리를 담당할 수 있는 반면, Filter는 여러 개가 함께 사용될 수 있다. 따라서 자신의 다음에 위치한 요소가 호출되지 않도록 필터링을 하거나, 요청이나 응답을 단계적으로 수정하는 것도 가능하다. Spring Security를 사용하면 Filter가 FilterChain에 등록되어 요청이 Servlet에 도달하기 전에 보안 관련 작업들을 수행해 준다.

 

서블릿 컨테이너는 표준을 따라 Filter 인스턴스를 등록할 수 있지만, 이때 등록되는 Filter는 단순 자바 객체로, Spring의 Bean이 아니다.  따라서, 스프링이 관리하는 객체나 DI를 사용할 수 없는 문제가 발생한다.

📌  DelegatingFilterProxy

Spring은 DelegatingFilterProxy라는 Filter 구현체로 해당 문제를 해결했다.

 

필터는 내부적으로 Spring ApplicationContext에서 지정한 Bean 이름을 찾아서 해당 Bean에게 처리를 위임한다. 즉, 실직적인 로직은 Spring Bean으로 등록된 필터 클래스가 담당하게 된다. 

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    Filter delegate = getFilterBean(someBeanName);
    delegate.doFilter(request, response);
}

DelegatingFilterProxy의 핵심 흐름

1. Spring Bean에게 위임한다

  • DelegatingFilterProxy는 프록시 역할
  • ApplicationContext에서 Bean으로 등록된 필터 객체를 찾아서, 해당 Bean의 doFilter() 호출
  • 서블릿 컨테이너와 스프링이 관리하는 Filter를 연결해 주는 역할

2. Lazy Lookup

 

  • 톰캣은 서버가 시작될 때 모든 필터를 등록
  • Spring의 ApplicationContext가 필터 등록보다 늦은 시점에 초기화되는 문제 발생
  • 서버 시작 시에는 껍데기로 등록만 하고, Spring Context가 뜨고 나면, 실제 Filter Bean을 찾아서 실행

📌  FilterChainProxy & SecurityFilterChain

FilterChainProxy는 SecurityFilterChain을 관리하고 실행하는 역할의 필터이다. 요청 경로에 맞는 SecurityFilterChain을 찾아서 그 필터 체인에 등록된 실제 필터들을 순서대로 실행하는 역할을 한다.

 

📌  Security Filters

Security Filter는 Spring Security에서의 최소 실행 단위로 볼 수 있다:

  • SecurityFilterChain은 여러 Security Filter들의 묶음
  • FilterChainProxy는 SecurityFilterChain을 실행하는 컨트롤러 같은 역할
  • 개별 Security Filter가 기능을 구현하는 가장 작은 실행 단위

선행 필터의 결과를 후속 필터가 의존하기 때문에, 순서가 틀리면 필터가 아예 작동하지 않거나, 보안이 무력화되는 위험이 있다. 따라서 실행 순서는 아주 중요한데-

 

공식 문서에서도 몰라도 된다고 하니까, 학습을 미래의 나에게 미루도록 하자 

절대 FilterOrderRegistration 코드 보고 뒷걸음질 친 거 맞음

 

 

🍟  Spring Security 적용하기


📌  ExceptionTranslationFilter

ExceptionTranslationFilter는 필터 체인에서 AuthenticationException이나 AccessDeniedException 같은 보안 예외를 HTTP 응답으로 변환해 주는 필터이다.

실행 흐름

  • 필터 체인의 중간 위치에서 다음 필터로 요청을 전달한다
  • 사용자가 로그인을 하지 않았거나, 인증 과정에서 예외가 발생했을 경우 (AuthenticationException)
    • SecurityContextHolder의 clearContext()를 호출하여 인증 정보를 삭제
    • 인증 성공 시 다시 redirect할 수 있도록 HttpServletRequest를 캐싱
    • AuthenticationEntryPoint 호출
  • 인증된 사용자지만, 권한이 부족한 경우 (AccessDeniedException)
    • AccessDeniedHanlder 호출
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    @Override
    public void commence(
            HttpServletRequest request, HttpServletResponse response, AuthenticationException authException
    ) throws IOException, ServletException {
        ErrorResponse errorResponse = new ErrorResponse("UNAUTHORIZED", "인증되지 않은 사용자입니다.");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        objectMapper.writeValue(response.getWriter(), errorResponse);
    }
}

@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(
            HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException
    ) throws IOException, ServletException {
        ErrorResponse errorResponse = new ErrorResponse("FORBIDDEN", "권한이 없는 사용자입니다.");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        objectMapper.writeValue(response.getWriter(), errorResponse);
    }
}

 

애플리케이션에 맞는 응답 형식으로 AuthenticationEntryPoint와 AccessDeniedHandler 인터페이스를 위와 같이 구현해 주었다.

 

📌  SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {
}

 

@EnableWebSecurity 애너테이션을 @Configuration 클래스에 추가하면 SecurityFilterChain Bean을 통해 정의된 Spring Security 설정을 적용할 수 있다.

.exceptionHandling(exception -> exception
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                        .accessDeniedHandler(jwtAccessDeniedHandler))
  • 인증 실패 -> JwtAuthenticationEntryPoint 사용
  • 인가 실패 -> JwtAccessDeniedHandler 사용
.csrf(AbstractHttpConfigurer::disable)

 

JWT 기반 인증에서는 클라이언트가 토큰을 헤더에 담아 보내기 때문에 불필요한 CSRF 보호 기능을 끈다.

.sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))

 

세션을 사용하지 않고 상태 없이 요청을 처리하기 때문에 Spring Security가 세션을 생성하거나 저장하지 않도록 설정한다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)

                .exceptionHandling(exception -> exception
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                        .accessDeniedHandler(jwtAccessDeniedHandler))

                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

 

설정이 완료된 SecurityFilterChain을 반환하면 Spring Security가 실제로 필터를 사용할 수 있게 된다.

 

🥸  투 비 컨티뉴드

아직 Authentication이랑 Authorization 부분은 하나도 언급하지 못했다는 럭키비키적 현실