미운 오리 새끼의 우아한 개발자되기

[Spring Boot] Spring Security 에서 특정 URL만 제외하여 필터링하기 본문

Spring & Spring Boot/Spring Boot

[Spring Boot] Spring Security 에서 특정 URL만 제외하여 필터링하기

Serina_Heo 2023. 1. 1. 11:14

제목은 특정 URL 만 제외하여 필터링하기라고 썼지만..

내가 쓴 방법은 일단 Filter에는 들어가는데 Filter 안에 로직을 타지 않도록 하여 Servlet Container 로 넘기는 방법이다.

 

Spring 의 구조는 이러하다.

나의 경우에는 JWT 로 인증/인가를 하고 있어서 JWT Filter 를 두었고, JWT 안에 있는 사용자 정보를 Controller 에서 추출하여 사용하고 싶어서 Spring Security 를 적용하였다. 

ref : https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter

 

내가 현재 만들고 있는 API 중에 /notifications 로 시작하는 endpoint 가 있다.

/notifications

GET   -- /{code}

POST -- /{code}

GET    -- /count 

이렇게 3개의 endpoint 가 있는데 마지막 /count 를 제외하고는 JWT Filter를 타서 사용자 정보를 SecurityContext 에 저장해주어야 한다는 것.

package com.hooonk.api.config;

import com.hooonk.api.config.filter.JwtAuthenticationFilter;
import com.hooonk.api.service.FirebaseService;
import com.hooonk.api.util.JwtTokenUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, FirebaseService firebaseService) {
        return new JwtAuthenticationFilter(jwtTokenUtil, firebaseService);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, JwtTokenUtil jwtTokenUtil, FirebaseService firebaseService) throws Exception {
        return http
                .httpBasic().disable()
                .csrf().disable()
                .cors().disable()
                .authorizeRequests()
                .antMatchers(
                        "/swagger-resources/**",
                        "/swagger-ui.html",
                        "/swagger-ui/index.html",
                        "/v3/api-docs",
                        "/webjars/**").permitAll()
                .antMatchers("/health/check").permitAll()
                .anyRequest().authenticated()
                .and()
                .requestMatchers().antMatchers("/account/**", "/notifications/**").and()
                .addFilterBefore(jwtAuthenticationFilter(jwtTokenUtil, firebaseService), BasicAuthenticationFilter.class)
                .build();
    }
}

위와 같이 requestMathers().antMatchers("/account/**", "/notifications/**").and().addFilter ~~

이렇게 처리하니까 notifications 으로 시작하는 모든 API 는 jwt Filter 를 탈수 밖에 없었다.

그렇다고 /notifications 에 속한 endpoints 중에 필요한 녀석만 써주자니 pathVariable 이 있어서 endpoint 를 특정할 수도 없다.

그래서 JWT Filter 에서 HttpServletRequest의 Servlet Path를 가져와서 JWT Filter 내부의 로직을 타지 않게 if else 문으로 처리하였다.

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        Authentication authentication;

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        try {
            String path = req.getServletPath();
            log.info("servletPath : " + path);

            if(!path.contains("/notifications/count")) {
                log.info("Don't pass here!!!!!!!!!!!!!!!!!!!!!!!!!!");
                String authToken = req.getHeader("Authorization");
                String bearerToken = authToken.substring(7);

                String userEmail = firebaseService.getUserUid(bearerToken);

                authentication = jwtTokenUtil.authenticate(new UsernamePasswordAuthenticationToken(userEmail, AuthorityUtils.createAuthorityList("USER")));
                log.info("authentication.getPrincipal().toString() ==> " + authentication.getPrincipal().toString());
                log.info("authentication.isAuthenticated() ==> " + authentication.isAuthenticated());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            log.info("이거지");
            chain.doFilter(request, response);
        } // catch문 이하 생략 ~~

그랬더니 if else 에 통과는 하는데.. 403 Forbidden 에러가 나는것...-_-?

띠껍게도 IDE Console에는 Error log 가 보이지 않는데 왜!! 403 이 뜨는거지?? 

2023-01-01 10:54:12.922 TRACE 49978 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/v1/error", parameters={}, headers={masked} in DispatcherServlet 'dispatcherServlet'
2023-01-01 10:54:12.925 TRACE 49978 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : 2 matching mappings: [{ [/error]}, { [/error], produces [text/html]}]
2023-01-01 10:54:12.925 TRACE 49978 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-01-01 10:54:12.931 TRACE 49978 --- [nio-8080-exec-1] o.s.web.method.HandlerMethod             : Arguments: [FirewalledRequest[ org.apache.catalina.core.ApplicationHttpRequest@5952c7a4]]
2023-01-01 10:54:12.939 DEBUG 49978 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
2023-01-01 10:54:12.940 TRACE 49978 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Sun Jan 01 10:54:12 KST 2023, status=403, error=Forbidden, path=/v1/notifications/count}]
2023-01-01 10:54:12.957 TRACE 49978 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : No view rendering, null ModelAndView returned.
2023-01-01 10:54:12.957 DEBUG 49978 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 403, headers={masked}

 

 이거저거 해보다가 Spring Security Configuration의 FilterChain에 이걸 추가하니까 정상 작동 하였다.

.antMatchers("/notifications/count").permitAll()

내가 구현한 JWT Filter 는 GenericFilter 를 extends 하여 구현한 거지만,

아마도..? 구현하지 않은 다른 필터들을 거칠 때 

permitAll 을 주지 않은 다른 request 들은 아래 구문에서 막힌게 아닌가 싶다.

.anyRequest().authenticated()

 

이번에 API 를 만들면서 Filter 와 Spring Security 를 좀 더 자세히 알게된 것 같다.