filterChain()
설정 메서드에서 사용함SecurityConfig
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests(
authorize -> authorize
.requestMatchers("/api/v1/user/login").permitAll()
.requestMatchers("/api/v1/user/join").permitAll()
.requestMatchers("/api/v1/user/reissue").permitAll()
.anyRequest().authenticated()
).apply(new JwtSecurityConfig(jwtTokenProvider));
return http.build();
}
}
이 에러를 보자마자 ‘어..? 내가 permitAll() 에 대해 잘 못 생각하고 있나..?’ 라고 느낌
근데 위에서 내가 “모든 요청은 허용한다” 라고 했는데..?
이 말은 반은 맞고 반은 틀림
왜냐하면 permitAll() 에 설정을 해도, Spring Security 에 있는 Filter Chain 은 반드시 거치기 때문임
그러면 왜 많은 블로그에서 permitAll() 이 URL 에 해당하는 요청을 허용한다는 의미일까?
그 이유는 요청이 Filter Chain 에 들어오지만, permitAll() 에 지정된 URL 일 경우,
SecurityContextHolder
에 보관되어 있는Authentication
객체에 해당 요청을 보낸 유저 정보가 없더라도 (당연히 없겠죠? 헤더에 토큰을 담고 요청을 보내지 않았으니까!!) 정상 흐름으로 처리하여 컨트롤러에 요청을 보내는 것임
뭐 어떻게 보면 요청을 허용하는 건 맞는듯?
근데 내 코드에선 에러가 뜨는 이유는, 아래의 코드를 보면
JwtTokenProvider
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
...
/**
* Access Token 검증
*/
public boolean validateToken(String token){
try{
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch(ExpiredJwtException e) {
log.error("Token 만료");
throw new RuntimeException("Token 만료. 재발급 필요");
} catch(JwtException e) {
log.error("잘못된 Access Token 타입");
throw new RuntimeException("잘못된 Access Token 타입");
} catch (IllegalArgumentException e) {
log.error("헤더가 비어있음");
throw new RuntimeException("헤더가 비어있음");
}
}
}
validateToken
: 토큰 검증 메서드catch(ExpiredJwtException e) {
log.error("Token 만료");
throw new RuntimeException("Token 만료. 재발급 필요");
} catch(JwtException e) {
log.error("잘못된 Access Token 타입");
throw new RuntimeException("잘못된 Access Token 타입");
} catch (IllegalArgumentException e) {
log.error("헤더가 비어있음");
throw new RuntimeException("헤더가 비어있음");
}
이런 식으로 검증을 시도하는 부분에서 직접 예외를 던짐
이런 식으로 해버리면 당연히 filter.doFilter()
로 넘어가서, 컨트롤러로 넘어가는 게 아니고, ExceptionTranslationFilter
로 넘어가는 것
물론 JwtFilter 의 doFilterInternal
메서드에서 따로 JwtAuthenticationException
과 같은 예외로 catch 해도 되긴 하지만 내가 상속받은 상위 클래스인 OncePerRequestFilter
엔 shouldNotFilter()
라는 메서드가 존재함
말 그대로 true 를 리턴하면 필터를 거치지 않는 것임
JwtFilter
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private static final List<String> EXCLUDE_URL =
Collections.unmodifiableList(
Arrays.asList(
"/api/v1/user/login",
"/api/v1/user/join",
"/api/v1/user/reissue"
));
...
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return EXCLUDE_URL.stream().anyMatch(exclude -> exclude.equalsIgnoreCase(request.getServletPath()));
}
...
}
OncePerRequestFilter
: 모든 서블릿에 일관된 요청을 처리하기 위해 사용하는 필터용 추상화 클래스EXCLUDE_URL
: 필터를 거치면 안되는 URL 을 담은 리스트shouldNotFilter
: OncePerRequestFilter
에 선언되어 있는 메서드로 지정된 요청의 필터링을 피하기 위해 true를 반환함
shouldNotFilter()
메서드를 재정의 함으로써 JwtFilter 으로 요청이 와도 URL 이EXCLUDE_URL
리스트에 존재하면 바로 true 를 반환함으로써 필터를 벗어나 바로 컨트롤러로 이동함