서큐리티를 도입하며 인증, 인가의 과정을 마쳤다.
이제 인증 및 인가 작업에서 발생하는 예외에 대해서 처리를 해주어야 하는데, 한 가지 생각해봐야 하는 문제가 있다.
스프링 서큐리티는 필터에 기반한 체이닝 구조이므로, 스프링 컨테이너까지 요청이 도달하지 않는다.
따라서, @ExceptionHandler를 통한 편한 예외 처리가 불가하다. 직접 Try - catch로 잡아서 응답을 내려주는 방법 밖엔 없다.
하지만, 스프링 서큐리티가 그렇게 허술하진 않다. 서큐리티 필터 체인의 구조를 보면 마지막 즈음에 예외를 처리하는 필터가 있는 것을 확인할 수 있다.
여기서 주로 사용하는 AuthenticationEntryPoint와 AccessDeniedHandler를 구현해 인증 및 인가 과정에서 일어난 예외에 대한 응답을 내려줄 수 있다.
인증 관련 예외 처리 AuthenticationEntryPoint
인증 관련 작업을 하며 수많은 예외를 처리하거나 던졌다.
예를 들어,
- UserDetails 객체를 생성하기 위해 DB에서 find를 할 때의 예외 - RuntimeException 던짐
- JWT 인증 과정 중 토큰 유효성 검사의 수많은 예외 - IllegalArgumentException 던짐
public boolean validateAccessToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey.getBytes()).build().parseClaimsJws(token);
return true;
} catch (MalformedJwtException e) {
throw new IllegalArgumentException(JwtErrorMessage.MALFORMED.getMessage());
} catch (ExpiredJwtException e) {
throw new IllegalArgumentException(JwtErrorMessage.EXPIRED.getMessage());
} catch (UnsupportedJwtException e) {
throw new IllegalArgumentException(JwtErrorMessage.UNSUPPORTED.getMessage());
} catch (ClassCastException e) {
throw new IllegalArgumentException(JwtErrorMessage.CLASS_CAST_FAIL.getMessage());
} catch (SignatureException e) {
throw new IllegalArgumentException(JwtErrorMessage.INVALID_SIGNATURE.getMessage());
} catch (Exception e) {
log.error("================================================ \n" +
"JwtValidator - validateAccessToken() 오류발생 \n" +
"token : " + token +
"\n Exception Message : " + e.getMessage() + "\n " +
"================================================");
throw new IllegalArgumentException(e.getMessage());
}
}
=> JWT 유효성 검사에서 예외 던지기
User dbUser = userRepository.findByLoginId(loginId).orElseThrow(() -> new UsernameNotFoundException(ResponseMessages.USER_NOT_EXISTS_MESSAGE.getMessage()));
=> UserDetails 객체 생성 과정 중 코드 -> 동일하게 예외를 던진다.
이러한 예외를 어디선가 받아서 응답을 내려주어야 한다.
이때, AuthenticationEntryPoint 클래스를 이용해 예외 처리가 가능하다.
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exceptionMessage = (String) request.getAttribute("exception");
List<String> list = Arrays.stream(JwtErrorMessage.values())
.map(JwtErrorMessage::getMessage)
.collect(Collectors.toList());
if (!list.contains(exceptionMessage)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print(exceptionMessage);
}
if (exceptionMessage.equals(JwtErrorMessage.MALFORMED.getMessage())) {
setResponse(response, JwtErrorMessage.MALFORMED);
} else if (exceptionMessage.equals(JwtErrorMessage.CLASS_CAST_FAIL.getMessage())) {
setResponse(response, JwtErrorMessage.CLASS_CAST_FAIL);
} else if (exceptionMessage.equals(JwtErrorMessage.EXPIRED.getMessage())) {
setResponse(response, JwtErrorMessage.EXPIRED);
} else if (exceptionMessage.equals(JwtErrorMessage.INVALID_SIGNATURE.getMessage())) {
setResponse(response, JwtErrorMessage.INVALID_SIGNATURE);
} else {
setResponse(response, JwtErrorMessage.UNSUPPORTED);
}
}
private void setResponse(HttpServletResponse response, JwtErrorMessage jwtErrorMessage) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print(jwtErrorMessage.getMessage());
}
}
=> commence() 를 오버라이드 해 구현하면 된다. 나 같은 경우는 JWT에 관한 예외 케이스가 많기 때문에 if문으로 분기 처리했고, 나머지 예외에 관해서는 JWT 예외를 모아둔 enum 클래스에 없는 객체인지를 검증하고 따로 response를 메시지와 함께 날려주는 형식으로 구현했다.
인가 관련 예외 처리 AccessDeniedHandler
인증 관련 작업에서 성공적으로 예외에 대한 처리 응답을 마쳤다.하지만, 인증이 완료되어도 인가에서 예외가 발생하며 그 즉시, 권한이 없는 응답과 함께 접근을 제한해야 한다. 이 경우는 AccessDeniedHandler 클래스를 구현하여 해결할 수 있다.
SecurityConfig에서 설정 했듯이, 각 url path마다 role을 설정할 수 있다. 여기서 반하는 권한의 요청이 들어왔을 때, 해결 가능하다.
ublic class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
setResponse(response);
}
private void setResponse(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print(SecurityMessage.PERMISSION_DENIED.getMessage());
}
}
=> 비교적 간단하게 권한이 없다는 응답 메시지를 내려주는 방식으로 구현했다.
이렇게 서큐리티를 이용해 인증, 인가를 구현했다. 좀 오래 걸렸고, 제대로 알고 하려다 보니 시간도 오래 걸렸다. 하지만 좋은 자양분이 되었음이 분명하다고 생각이 된다.이제 본격적으로 서비스 로직을 다루면서 리팩토링을 진행한다. 파이팅~
https://github.com/GroovyArea/My-ChickenBreast-Shop
'📕 Spring Framework > Spring 개념 정리' 카테고리의 다른 글
[@DataJpaTest] h2 인메모리 db를 이용한 테스트 설정 방법 (0) | 2022.12.21 |
---|---|
WebFlux는 무엇이고, 왜 나왔고, 언제 쓰이는가? (0) | 2022.08.31 |
Spring Security [1] - JWT를 이용한 REST API 인증과 인가 (0) | 2022.08.18 |
2022.05.17 「@Transactional 옵션 및 성능」 (0) | 2022.05.17 |
2022.05.13 「Session과 Token(Jwt) & 인증과 인가」 (0) | 2022.05.13 |