본문 바로가기
Spring

[Spring] SpringSecurity 403에러 해결

by worldcenter 2024. 11. 20.

예외 발생

SpringSecurity 설정과 JwtFilter를 아래와 같이 설정하고 앱 실행 후 회원 가입(/auth/signup)을 진행하면 403 에러가 발생합니다.

// Spring Security Config

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(securedEnabled = true)
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl UserDetailsServiceImpl;

    @Bean
    public JwtFilter jwtFilter() {
        return new JwtFilter(jwtUtil, UserDetailsServiceImpl);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.csrf(AbstractHttpConfigurer::disable);

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

        httpSecurity.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers("/auth/**").permitAll()
                        .requestMatchers("/admin/**").hasAuthority("ADMIN")
                        .anyRequest().authenticated()
        );


        httpSecurity.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl UserDetailsServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        String url = request.getRequestURI();
        String bearerJwt = request.getHeader("Authorization");

        if (bearerJwt == null) {
            // 토큰이 없는 경우 400을 반환합니다.
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {

            // JWT 유효성 검사와 claims 추출
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
                return;
            }

            setAuthentication(Long.valueOf(claims.getSubject()));
            chain.doFilter(request, response);

        } catch (SecurityException | MalformedJwtException e) {
          ...
        }


}

 

이미 Spring Security에서 /auth 로 시작하는 API는 permitAll() 로 익명 사용자에게 접근 권한을 허용했는데 왜 403 에러가 발생하는 것일까요?

 

 

원인 파악

디버깅을 통해 확인한 결과 Spring Security Config에서 /auth/** 를 permitAll() 해도 여전히 Filter를 타는 것을 확인할 수 있었습니다.

기존에 알고 있는 지식은 permitAll() 로 전체 접근 권한을 허용하면 인증을 거치지 않기 때문에 Filter도 타지 않는 것으로 알고 있었습니다. 하지만 제가 잘못 알고 있었다는 걸 깨달았습니다.

 

SpringSecurity 설정을 통해 특정 API의 접근권한을 허용하더라도 Filter를 타게 됩니다.

그렇다면 permitAll()은 어떤 기능을 하는 걸까요?

permitAll()은 특정한 경로나 리소스에 대해서 모든 사용자(익명)에 대해 인증을 요구하지 않고 액세스를 허용합니다.

 

여기서 모든 사용자에게 인증을 요구하지 않는다는 말이 무엇일까요?

먼저 SecurityContext에 대해서 알아야 합니다.
SecurityContext는 SpringSecurity에서 현재 사용자의 보안 정보를 저장하고 제공하는 인터페이스 입니다.
주로 Authentication 객체를 저장하고, 이를 통해 현재 사용자의 인증 상태와 권한 정보에 접근할 수 있게 해줍니다.
SecurityContextHoler를 통해 SecurityContext에 접근하고 현재 Authentication을 얻을 수 있습니다. 이를 통해 현재 사용자의 인증 정보 및 권한 정보를 얻을 수 있습니다.
permitAll()을 사용했을 경우 모든 필터 체인을 거친 후 SecurityContextHolder 안에 존재하는 SecurityContext에 Authentication 인증 객체가 존재하지 않아도 해당 API 호출이 정상적으로 가능하게 해줍니다.
(Filter는 타지만 인증 객체가 존재하지 않아도 프로세스가 정상적으로 동작하게 해준다는 의미)

 

따라서, 필터를 거치기 때문에 필터 안에 /auth/** 에 대한 예외처리가 없어 403 에러가 발생하는 것 입니다.

 

 

해결 방법

1) Filter 내 예외 처리

Filter 내 /auth 로 시작하는 API에 대해서는 해당 필터를 거치지 않도록 하는 코드를 넣습니다.

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl UserDetailsServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        String url = request.getRequestURI();
        String bearerJwt = request.getHeader("Authorization");

        // /auth로 시작하는 API의 경우 filter 패스
        if (url.startsWith("/auth")) {
            chain.doFilter(request, response);
            return;
        }

        if (bearerJwt == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);
        
        ...


}

 

2) shouldNotFilter 적용

Custom 필터 내에서 shouldNotFilter를 override하여 등록해주면 됩니다. 1번 방법과 비슷한 방식입니다.

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl UserDetailsServiceImpl;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        String url = request.getRequestURI();
        String bearerJwt = request.getHeader("Authorization")
        
        ...
    }
    
    // shouldNotFilter 적용
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return StringUtils.startsWithIgnoreCase(request.getRequestURI(), "/auth");
    }


}

 

3) WebSecurityCustomizer

Spring Security Config에다가 WebSecurityCustomizer를 Bean으로 등록하면 됩니다. 이렇게 하는 이유는 WebSecurity가 HttpSecurity보다 상위에 존재하기 때문에 WebSecurity의 ignoring에 경로를 적으면 Spring Security의 필터 체인이 적용되지 않습니다.

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
       return web -> {
           web.ignoring()
               .antMatchers("/auth/**"); // 필터를 타면 안되는 경로 
       };
 }

 

단, Custom 필터 사용시 해당 방법은 사용이 불가합니다.

 

 


참조 링크

https://velog.io/@choidongkuen/Spring-Security-SecurityConfig-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-permitAll-%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EC%9D%B4%EC%9C%A0

 

[Spring Security] - SecurityConfig 클래스의 permitAll() 이 적용되지 않았던 이유

안녕하세요 이번 포스팅에서는 Better 팀의 Iter 프로젝트 에서 진행했던 Spring Security 을 이용한 회원 인증/인가 시스템에서 제가 겪었던 문제점과 새롭게 알게된 점을 주제로 작성하고자합니다

velog.io

https://suhyeon-developer.tistory.com/42

 

[SpringBoot] Spring Security Config에서 permitAll()에 대한 진실과 오해

사용 기술 스택 - Spring Boot 3.x.x - Spring Security 6 ( + jwt token 방식) - Spring Data Jpa 문제 발생 Spring Security를 적용하면서 인증이 필요하지 않은 경로를 직접 지정해주어 인증을 안하도록 설정하였다. 대

suhyeon-developer.tistory.com