Spring Security 예외처리( AuthenticationEntryPoint, AccessDeniedHandler, unsuccessfulAuthentication )
관련 포스트
2023.07.13 - [Spring/Security] - 스프링) Spring Security Authentication/Authorization (23-06-29)
스프링) Spring Security Authentication/Authorization (23-06-29)
velog에서 이전한 글 입니다. Filter Spring의 life cycle이다. Security Spring Security도 많은 filter로 구성 돼있다. Security의 목적은 controller에서 인증/인가를 분리하고 filter단에서 손쉽게 처리하기 위함이다.
cornpip.tistory.com
Spring Security 예외처리

현재 구성한 인증/인가 흐름이다. 프론트에서 error msg를 요청했고 현재 흐름에서 핸들링 할 수 있는 부분을 찾고 처리했다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
....
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling((exceptionHandling) ->
exceptionHandling
.authenticationEntryPoint(myAuthenticationEntryPoint())
.accessDeniedHandler(myAccessDeniedHandler())
);
...
return http.build();
}
버전은 boot-start-security:3.1.2 이고 필터와 에러 핸들링 설정은 위와 같다. 예외 처리를 하나씩 살펴보자
(a)
switch (jwtUtil.validateToken(tokenValue)) {
case "fail" -> {
log.info("case fail");
responseUtil.responseToExceptionResponseDto(response, HttpStatus.FORBIDDEN, "토큰 유효성 검증에 실패했습니다.");
}
...
}
유효성 검증에서 fail을 던지면 HttpServletResponse를 처리하고 filterChain.doFilter(request, response) 로 필터를 이어가지 않고 끝났다. 즉 에러 핸들링이 Security가 아닌 JwtAuthorizationFilter에서 진행된다.
(b)
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
...
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info(failed.getMessage());
responseUtil.responseToExceptionResponseDto(response, HttpStatus.UNAUTHORIZED, failed.getMessage());
}
...
}
로그인 라우트로 들어온 경우 예외는 UsernamePasswordAuthenticationFilter 의 unsuccessfulAuthentication에서 핸들링한다. 이 경우 AuthenticationEntryPoint로 이어지지 않고 만약 unsuccessfulAuthentication을 오버라이딩하지 않았다면 AuthenticationEntryPoint에서 처리된다.
(c)
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ResponseUtil responseUtil;
@Autowired
public MyAuthenticationEntryPoint(ResponseUtil responseUtil) {
this.responseUtil = responseUtil;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//인증 실패, 없는 라우트 등 잘못된 요청일 경우
responseUtil.responseToExceptionResponseDto(response, HttpStatus.FORBIDDEN, "잘못된 요청입니다.");
}
}
AuthenticationEntryPoint는.anyRequest().authenticated() 을 통과하지 못했을 때 최종 핸들링이라고 볼 수 있다.
통과 못 할 케이스는
- 인가에서 setContext가 문제가 있을 때
- permitAll이 아닌데 Context에 인증 정보가 비어있을 때
- 없는 라우트이거나 요청 형식이 올바르지 않을 때
정도를 예상해 볼 수 있다.
(d)
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private final ResponseUtil responseUtil;
@Autowired
public MyAccessDeniedHandler(ResponseUtil responseUtil) {
this.responseUtil = responseUtil;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
responseUtil.responseToExceptionResponseDto(response, HttpStatus.FORBIDDEN, "인가 실패");
}
}
setContext가 정상적인 상태에서 인가가 허락되지 않는다면 처리하는 핸들링으로 ROLE_USER가 ROLE_ADMIN 라우트에 접근하는 케이스이다.
문제
현재 구성에서 문제는 (a)이다. 토큰이 있으면 permitAll이던 /authenticated route이던 인가 필터에서 에러핸들링을 하고 끝난다. 토큰이 있어도 permitAll과 login은 토큰 유효성과 무관하게 진행했으면 한다.
해결
public static final String[] permitAllRouteArray = {
"/api/user/signup", "/api/user/cors", "/api/user/kakao/login", "/swagger-ui/**", "/webjars/**",
"/v3/api-docs/**", "/swagger-resources/**", "/api/file/**"
};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
....
http.authorizeHttpRequests((authorizeHttpRequest) ->
authorizeHttpRequest
.requestMatchers(permitAllRouteArray).permitAll()
.anyRequest().authenticated()
);
....
}
Security에 permitAll한 라우트를 담고있는 무언가가 있을 것 같지만 일단 Security에 위와 같이 static 으로 permitAll할 라우트를 선언 할당하고 requestMatchers를 설정했다.
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String loginUrl = "/api/user/login";
...
@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil, ResponseUtil responseUtil) {
...
setFilterProcessesUrl(JwtAuthenticationFilter.loginUrl);
}
...
}
login route도 static 변수를 선언 할당했다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getTokenFromRequest(request);
boolean loginPass = JwtAuthenticationFilter.loginUrl.equals(request.getServletPath());
boolean permitPass = Arrays.stream(WebSecurityConfig.permitAllRouteArray)
.anyMatch(str -> str.equals(request.getServletPath()));
if (loginPass || permitPass || !StringUtils.hasText(tokenValue)) {
filterChain.doFilter(request, response);
} else if (StringUtils.hasText(tokenValue)) {
...
}
}
OncePerRequestFilter를 상속받은 JwtAuthorizationFilter 에서 위와같이 요청 라우트(request.getServletPath)가 loginUrl이나 permitAllRouteArray에 있는 라우트 중 하나와 같은지 확인하고 같은 경우 JwtAuthorizationFilter에서 유효성을 검증하고 setContext 하는 작업을 건너뛴다.
'back-end > spring' 카테고리의 다른 글
| Json 직렬/역직렬, @RequestBody, @ModelAttribute (0) | 2023.08.04 |
|---|---|
| spring data JPA - slice, page (무한 스크롤, 페이지네이션) (0) | 2023.07.27 |
| Spring boot DI 주입 방식 (필드/수정자/생성자) (0) | 2023.07.22 |
| Spring Security Cors 에러( WebMvcConfigurer/ corsConfigurationSource ) (0) | 2023.07.20 |
| 스프링) JPA Entity option 지연 로딩/영속성 전이/고아 entity삭제 (23-07-12) (0) | 2023.07.14 |