본문 바로가기

Development/Spring

[Spring] Spring Security + JWT: SecurityFilterChain, AccessDeniedHandler, AuthenticationEntryPoint

SecurityFilterChain

  • Spring Security(버전 5.7 이상)에서 SecurityFilterChain은 웹 애플리케이션을 보호하는 Filter Chain을 나타내는 Bean이다.
  • 이 Filter들은 인증, 권한 부여, 세션 관리 등 다양한 보안 작업을 처리한다.

WebSecurityConfigurationAdapter?

WebSecurityConfigurerAdapter은 Spring Security 5.7 이후로 더이상 사용되지 않는다.(deprecated)
대신, SecurityFilterChain 이라는 Bean을 등록하여 구현하는 방식이 사용된다.

SecurityFilterChain의 작동 방식

  1. Spring Security는 애플리케이션 시작 시 SecurityFilterChain Bean을 생성한다.
  2. FilterChainProxy는 들어오는 HTTP 요청을 가로채옵니다.
  3. 설정된 체인을 기반으로 FilterChainProxy는 순서대로 각 필터를 호출한다. 각 필터는 보안 검사(예: 인증, 권한 부여)를 수행하며 요청이나 응답을 수정할 수도 있다.
  4. 필터가 요청 진행을 허용하면 대상 리소스(ex: Controller)에 도달한다.

SecurityConfiguration

  • SecurityConfiguration는 Spring Security와 관련된 설정을 하는 클래스이다.
  • Spring Security를 설정하는 대표적인 방법은 @Configuration 클래스를 구현하는 것이다.
@Configuration
//@EnableWebSecurity // Spring Security에 대한 디버깅 모드를 사용하기 위한 어노테이션 (default : false)
public class SecurityConfiguration {

    private final JwtTokenProvider jwtTokenProvider;

    @Autowired
    public SecurityConfiguration(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.httpBasic(HttpBasicConfigurer::disable) // REST API는 UI를 사용하지 않으므로 기본설정을 비활성화
                .csrf(CsrfConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션을 생성하지 않도록함
                .authorizeHttpRequests(authorize -> authorize // 리퀘스트에 대한 사용권한 체크
                        .requestMatchers("/sign-api/sign-in", "/sign-api/sign-up",
                        "/sign-api/exception").permitAll() // 가입 및 로그인 주소는 허용
                        .requestMatchers(HttpMethod.GET, "/product/**").permitAll() //product로 시작하는 Get 요청은 허용
                        .requestMatchers("**exception**").permitAll()
                        .anyRequest().hasRole("ADMIN") // 나머지 요청은 인증된 ADMIN만 접근 가능)
                )
                .exceptionHandling((exception) -> exception.accessDeniedHandler(new CustomAccessDeniedHandler())) // 권한 확인 과정에서 통과하지 못하는 예외가 발생할 경우 예외 클래스 전달
                .exceptionHandling((exception) -> exception.authenticationEntryPoint(new CustomAuthenticationEntryPoint())) // 인증 과정에서 예외가 발생할 경우 예외를 전달
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class); // JWT Token 필터를 id/password 인증 필터 이전에 추가
        return httpSecurity.build();
    }

HttpSecurity

HttpSecurity는 특정 HTTP 요청에서 보안 설정을 위한 메서드들을 제공한다.
대표적인 기능은 다음과 같다.

  • 리소스 접근 권한 설정
  • 인증 실패 시 발생하는 예외 처리
  • 인증 로직 커스터마이징
  • csrf, cors 등의 스프링 시큐리티 설정

WebSecurity

WebSecurity는 HttpSecurity 앞단에 적영되며, 인증과 인가가 모두 적용되기 전에 동작하는 설정을 제공한다.
따라서 인증과 인가가 적용되지 않는 리소스 접근에 대해서만 사용한다.
WebSecurityCustomizer 인터페이스를 Bean에 등록하고 구현하여 설정이 가능하다.
아래는 예제 코드이다.

@Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring().requestMatchers(
                "/v3/api-docs/**", "/swagger-resources/**",
                "/swagger-ui.html", "/webjars/**", "/swagger/**", "/sign-api/exception", "/swagger-ui/**"
        );
    }
  • Swagger UI 및 API 문서 접근 허용: Swagger UI 및 SpringDoc OpenAPI(/v3/api-docs)와 관련된 URL 패턴을 포함하여, 해당 리소스에 누구나 자유롭게 접근할 수 있다.
  • 예외 처리 엔드포인트 허용: "/sign-api/exception" 경로를 보안 검사에서 제외하여 예외 처리와 관련된 API도 인증 없이 접근할 수 있다.

전체 소스 코드:
GITHUB

커스텀 AccessDeniedHandler, AuthenticationEntryPoint 구현

AccessDeniedHandler

AccessDeniedHandler는 사용자가 특정 리소스에 접근하려고 할 때 권한이 부족한 경우 처리하는 핸들러이다.

즉, 사용자가 인증되었지만 요청한 리소스에 대한 권한이 없는 경우 AccessDeniedHandler가 작동한다.

AccessDeniedHandler를 구현하려면 handle 메서드를 구현해야 한다.

아래 코드는 Spring Security에서 사용자가 권한이 없는 리소스에 접근하려고 시도할 때 사용자를 "/sign-api/exception" 경로로 리다이렉트하도록 동작한다.

리다이렉트가 되면 다른 스레드에서 동작이 된다.

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
        AccessDeniedException exception) throws IOException {
        LOGGER.info("[handle] 접근이 막혔을 경우 경로 리다이렉트");
        response.sendRedirect("/sign-api/exception");
    }
}

AuthenticationEntryPoint

Spring Security AuthenticationEntryPoint는 사용자가 인증되지 않은 상태에서 인증이 필요한 리소스에 접근하려고 할 때 처리하는 핸들러이다.

즉, 사용자가 로그인하지 않은 상태에서 접근 제어가 설정된 리소스에 요청을 보낼 때 AuthenticationEntryPoint가 작동한다.

 

AuthenticationEntryPoint를 커스텀하기 위해서는 commence() 메서드를 오버라이딩하여 구현해야 한다.

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException ex) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

위 코드는 인증 실패 시 Response로 인증 실패 응답 코드를 반환하는 동작을 한다.

만약 직접 Response를 생성하고 에러 메시지를 리턴하고 싶다면 다음과 같이 구현할 수 있다.

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException ex) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        EntryPointErrorResponse entryPointErrorResponse = new EntryPointErrorResponse();
        entryPointErrorResponse.setMsg("Authentication failed");

        response.setStatus(401);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(entryPointErrorResponse));
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EntryPointErrorResponse {

    private String msg;

}

위 예제 코드에서는 직접 Response를 생성하여 클라이언트에게 응답하는 방식으로 구현되어 있다.

  • 메시지를 담기 위해 EntryPointErrorResponse 객체를 사용하여 메시지를 설정하고, response에 상태 코드(status), 콘텐츠 타입(Content-type) 을 설정한다.
  • 그 후 ObjectMapper를 사용해 EntryPointErrorResponse 객체를 바디 값으로 파싱한다.

참고