작성
·
813
·
수정됨
0
안녕하세요 호돌맨님 강의 항상 잘보고있습니다. 다름이 아니라 실습 중 의도치 않게 동작하는 부분이 있어 질문드립니다.
상황
회원 가입 후 로그인
이 때 유저의 Role 은 ADMIN
메소드 시큐리티로 아래와 같이 자원의 권한 제한
@RestController
class HomeController {
@GetMapping("/user")
@PreAuthorize("hasRole('ROLE_USER')")
fun user(): String {
return "user 접근 가능👁"
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
fun admin(): String {
return "admin 접근 가능 👨💼"
}
}
이 때
ExceptionHandler로 Runtime예외를 캐치해 응답을 주고 있습니다.
동시에 커스텀한 403핸들러를 HttpSecurity에 끼워 넣어주었습니다.
// ControllerAdvice
@ExceptionHandler(Exception::class)
fun handleRuntimeException(ex: Exception): ResponseEntity<ErrorResult> {
logger.error("ex", ex)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
ErrorResult(
code = "500",
message = ex.message,
)
)
}
// SecurityConfig
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
return http
// .. other config..
.exceptionHandling { e ->
e.accessDeniedHandler(Http403Handler(objectMapper))
e.authenticationEntryPoint(Http401Handler(objectMapper))
}
.build()
}
ADMIN으로 로그인한 유저가 USER 자원에 접근하면 아래와 같이 응답
{
"code": "403",
"message": "접근할 수 없습니다.",
"validation": null
}
{
"code": "500",
"message": "Access Denied",
"validation": null
}
ControllerAdvice에서 Runtime예외를 처리하지 않는다면 의도대로 403이 응답되는데, ControllerAdvice에서 예외를 포괄적으로 처리하게 되면 403이 응답되지 않습니다.
혹시 이런경우를 겪으신적이 있는지? 따로 해결방법이 있을지 궁금해 여쭙습니다.
감사합니다.
답변 2
1
안녕하세요. 호돌맨입니다.
질문을 깔끔하게 남겨주셔서 감사합니다.
제가 지금 지하철이라 정확히 파악은 어려운데
그래도 함 살펴보자면..
AdviceController에서 Exception을 잡으시면 모든 예외를 처리하게 될텐뎅..
AccessDeniedException을 AdviceController에서 잡아버려 500이 발생한 게 아닐까용?
디버거로 찍어서 실행했을때
AdviceController에 handleRuntimeException 메서드에서 걸리는지 확인해보면 좋을것 같습니다.
혹시 그게 아니라면
AccessDenied Exception쪽 클래스 내용을 보여주시면 도움될것 같습니다.
0
저만 겪는 경우가 아니라 상황 공유합니다.
해결책은 ControllerAdvice에서 Access Denied Exception를 잡는 방법이라고 합니다.
https://stackoverflow.com/questions/72615257/spring-accessdeniedhandler-interface-dont-get-called-when-i-have-exceptionhandl
https://github.com/spring-projects/spring-security/issues/6908
디버깅을 해보니 아래와 같습니다.
@MethodSecurity
는 AOP를 이용하여 권한을 체크합니다.(Spring MVC계층까지 전파)
이 때 권한 에러가 발생한다면 @ControllerAdvice
에서 에러를 캐치하여 응답을 내리기 때문에 SecurityFilterChain
에서 AccessDeniedException
이 발생함을 인지하지 못하여 그대로 Advice로직이 실행됩니다.
하지만 @ControllerAdvice
를 비활성화한다면 MVC 계층에서 발생한 AccessDeniedException
이 그대로 SecurityFilterChain
까지 올라가 커스텀하게 구현한 AccessDeniedHandler(Http403Handler)
로직이 동작합니다.
앞으로도 좋은 강의 기대하겠습니다. 감사합니다.
더불어 MethodSecurity가 아닌
auth.requestMatchers("/").permitAll()
.requestMatchers("/user").hasRole("USER")
.requestMatchers("/admin").hasRole("ADMIN")
형태로 직접 작성해주면 ControllerAdvice가 활성화 되어도
정상적으로 AccessDeniedHandler(Http403Handler)
로직이 동작합니다.
다 찾아내시다니.. 정말 멋지시네요!
그러게요 이게 좀 애매합니다.
AccessDeniedException, AuthenticationException 등의 스프링 시큐리티 Exception들이 어떤 특정 Class를 상속받고 있다면 문제 해결이 쉬운데
모두 RuntimeException을 상속받고 있습니다.
그래서 현재는 아래와같이 분기문을 통해 status Code를 결정하는게 최선 같아보입니다.
아 이러면 코드가 안예쁜데..
@ResponseBody
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> exception(Exception e) {
log.error("예외발생", e);
int errorCode = 500;
if (e instanceof AccessDeniedException) {
errorCode = 403;
} else if (...) { }
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(errorCode))
.message(e.getMessage())
.build();
ResponseEntity<ErrorResponse> response = ResponseEntity.status(errorCode)
.body(body);
return response;
}
아니면 아래와같이 하는 방법도 있을것 같습니다.
enum 형태의 SpringSecurityErrorCode.java 를 만들고요
@RequiredArgsConstructor
public enum SpringSecurityErrorCode {
ACCESS_DENIED(AccessDeniedException.class, 403),
UNAUTHORIZED(AuthenticationException.class, 401),
// 기타등등 ...
ETC(RuntimeException.class, 500);
private final Class<? extends RuntimeException> exceptionClass;
private final int statusCode;
public static int getStatusCode(RuntimeException springException) {
return Arrays.stream(values())
.filter(e -> e.exceptionClass.isAssignableFrom(springException.getClass()))
.findAny()
.map(e -> e.statusCode)
.orElse(500);
}
}
아래처럼 예외에 맞는 status를 찾아 응답하는겁니다.
@ResponseBody
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> exception(RuntimeException e) {
log.error("예외발생", e);
var statusCode = SpringSecurityErrorCode.getStatusCode(e);
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(statusCode))
.message(e.getMessage())
.build();
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
멋지십니다!
감사합니다.
지하철에서까지 빠른 답변 감사합니다.
저도 말씀하신 것처럼 예상이 됩니다. 그렇다면 ControllerAdvice에서 RuntimeExeption을 잡고자한다면 필연적으로 AccessDeniedExption은 AccessDeniedHandler 구현체가 아닌 (HttpSecurity에 끼워넣는 것이 아닌) ControllerAdvice에서 따로 잡아야 할까요?
호돌맨님이라면 어떻게 하실지가 궁금합니다!