SpringSecurity permitAll이 동작하지 않는다!

Choi Wontak·2025년 5월 9일

아이쿠MSA

목록 보기
9/12
post-thumbnail

난이도 ⭐️⭐️
작성 날짜 2025.05.10

고민 내용

현재 아이쿠는 Security 기반의 JWT Token을 사용하고 있다.
회원가입과 같은 경우 토큰 발급이 아직이기 때문에 요청 자체를 열어두어야 하고, 리프레시 토큰을 이용한 토큰 재발급의 경우에는 Access 토큰 자체가 만료되었을 테니 필터링하면 안된다.

그런데 SecurityConfig에서 permitAll로 설정한 경로들이 토큰 없이는 FORBIDDEN 오류가 뜨는 것이다!

🤔
분명히 PermitAll 설정을 걸었는데 왜 검문을 할까?


찾아보기

문제 인식

Security에서 필터를 거치는 과정에서 문제가 발생한 것 같다.
permitAll의 역할은 인증이 아닌 인가 과정에서 모든 사용자를 접근 가능하게 한다는 것인데,
이 부분을 인증 절차에서도 모두 접근 허용으로 착각하는 바람에 생긴 문제였다.
따라서 permitAll을 설정하더라도, 필터는 설정한 대로 넘어가 인증 과정을 거친다.

원래의 JwtAuthenticationFilter는 다음과 같다.

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String token = resolveToken(exchange);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContext context = new SecurityContextImpl(authentication);

            ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                    .header("Access-Member-Id", MDC.get("accessMemberId"))
                    .build();

            ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
            return chain.filter(mutatedExchange)
                    .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context)));
        }

        return chain.filter(exchange);
}

여기서 resolveToken 부분이 AUTHORIZATION 헤더를 가져오면서 생기는 1차 문제,
validateToken이 만료 기간을 검증하면서 생기는 2차 문제를 확인할 수 있다.

해결책 알아보기

여기서 세 가지 선택지가 있는데,

  1. 필터를 인가 이후로 보내 permitAll 경로는 필터를 우회하도록 한다.
  2. shouldNotFilter를 이용한다.
  3. 필터 내부에서 경로에 따라 필터링 한다.

1번은 의미상 가능하긴 하지만...
JwtAuthenticationWebFilterSecurityContext를 넣어주어야 뒤에 단계에서 AuthorizationWebFilter를 통해 인가 과정이 가능한데, 이것의 순서를 바꿔주는 과정이다 보니 문제가 생긴다.

=> 따라서 탈락.

2번은 다른 블로그에서 많이 찾아볼 수 있었던 해결책이었다.
(참고 : https://velog.io/@choidongkuen/Spring-Security-SecurityConfig-클래스의-permitAll-이-적용되지-않았던-이유)

스프링 WebMVC에서는 OncePerRequestFilter를 상속해서 필터를 커스텀화한다.
그러나 우리는 MSA 환경에서 요청을 적절한 서비스로 전달하는 Spring Gateway에서 토큰을 검증하고 있었고, Webflux 환경이었기 때문에 WebFilter를 상속받고 있었다.
이 클래스는 filter 메소드만을 사용하기 때문에 shouldNotFilter라는 메소드가 없었다.

=> 따라서 얘도 탈락.

남은 것은 3번

필터 내부에서 if문을 이용해 해당 패턴에 일치하면 필터 로직을 거치지 않고 바로 다음 필터로 넘기는 방식.

중복도 늘어날 것 같고 복잡해보여서 다른 방식을 찾아보고 싶었지만, 거의 유일한 방법인 듯해 최대한 효율적인 방법을 생각해서 작성해보기로 했다.

해결책 적용해보기

우선 걱정되었던 중복은 다음에서 발생한다.

  • SecurityConfig에서 PermitAll하는 요청
  • 필터에서 예외처리 해줄 요청

두 요청은 같은 요청인데, 다른 곳에 숨겨져 있으면 나중에 열린 요청이 늘어나는 경우 누락의 위험이 있다.

public static final String[] ALL_METHOD_PERMIT_ALL_PATHS = {
            "/login/sign-in/**",
            // 생략 ...
    };

public static final String[] POST_METHOD_PERMIT_ALL_PATHS = {
            "/users",
            // 생략 ...
};

상수로 빼주었다.
POST만 처리해 줄 경로도 있어서 따로 빼주었다.
SecurityConfig의 pathMatchers는 String[] 형식도 받아주기 때문에 이걸 그대로 넣어주면 된다.

.pathMatchers(JwtSecurityUtils.ALL_METHOD_PERMIT_ALL_PATHS).permitAll() 
.pathMatchers(HttpMethod.POST, JwtSecurityUtils.POST_METHOD_PERMIT_ALL_PATHS).permitAll()

이런 느낌...
이전 코드와 크게 다르지 않지만 가독성은 더 올라간 것 같다.
SecurityConfig 쪽은 끝.

다음과 같은 Util 함수를 추가해주었다.

public static boolean isPermitAllPath(String path, HttpMethod method) {
        if (HttpMethod.POST.equals(method)) {
            return Arrays.stream(POST_METHOD_PERMIT_ALL_PATHS)
                    .anyMatch(pattern -> pathMatcher.match(pattern, path));
        }

        return Arrays.stream(ALL_METHOD_PERMIT_ALL_PATHS)
                .anyMatch(pattern -> pathMatcher.match(pattern, path));
}

메소드와 경로를 통해 해당 Permit 조건에 부합하는지 확인한다.

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getPath().toString();
        HttpMethod method = exchange.getRequest().getMethod();

        if (JwtSecurityUtils.isPermitAllPath(path, method)) {
            return chain.filter(exchange);
        }
        // 후략 ...

그리고 이 Util 함수를 통해 필터 로직을 수행하기 전 앞 쪽에서 확인시켰다.
만약 부합하는 경우 필터 체인을 바로 넘긴다.


결론

이제 허용 경로가 늘어나는 경우에도 상수 처리한 리스트에 한 줄만 추가하면 된다.
더이상 FORBIDDEN도 발생하지 않으니 문제 해결!

결론
Security는 편리하지만 거대하고 어려운 것 같다.
잘 공부하고 사용하자! 😭


profile
백엔드 주니어 주니어 개발자

0개의 댓글