로그인, 로그인 상태 검증 등 인증과 관련된 내용을 정리하다보니 인증에 실패했을때 리턴 형태에 대해 고민하게 되었다.
지금은 기본 지정된 상태로 값이 리턴되고 있다.
이걸 에러코드 / 메세지로 깔끔하게 보내는건 어떨까? 하는 생각이 들었다.
인증과 관련해서 발생할 수 있는 에러는 크게 두가지로 나뉜다.
먼저 로그인(Authentication)과정에서 실패하는 경우 -> AuthenticationFailureHandler를 통해 원하는 작업을 지정할 수 있다.
그 외 인증 과정에서 만료된 토큰이나 조작된 토큰 등 유효하지 않은 인증 정보를 사용하는 경우 -> 발생한 에러를 AuthenticationEntryPoint에서 잡아서 처리할 수 있다.
먼저 로그인에 실패했을때 에러 코드 / 메세지를 출력할 수 있도록 하였다.
(처음엔... 인증 관련 모든 예외는 AuthenticationEntryPoint에서 처리하는줄알고 Postman으로 계~속 테스트를 했는데 안돼서...
이것저것 알아보다가 AuthenticationFailureHandler가 따로 있다는 것을 알게되었다...ㅎㅎ)
인증과정에서 실패했을때도 에러코드 / 메세지를 출력하도록 하였다.
[공통처리 (로그인 실패, 인증 실패)]
1. ErrorResponse: 예외 발생시 status, message를 리턴할 수 있도록 하였다.
생성자는 private으로 하고 of() 메서드를 통해 생성하도록 하였다.
-> ErrorResponse 클래스는 인증 외에도 다른 예외상황에서도 사용할 예정이므로, 필드 추가 등 코드는 변경될 예정이다.
public class ErrorResponse {
private int status;
private String message;
private ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
public static ErrorResponse of(HttpStatus httpStatus) {
return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
}
}
[로그인 실패 처리]
1. MemberAuthenticationFailureHandler: 로그인 인증에 실패했을때 작동할 핸들러이다.
AuthenticationFailureHandler를 구현한 클래스로 작성하였다.
AuthenticationFailureHandler는 Spring Security에서 인증에 실패했을 때 호출되는 핸들러 메서드이다.
onAuthenticatinoFailure() 메서드를 오버라이드하여 원하는 작업을 설정해줄 수 있다.
현재 코드에서는 로그를 남기고, 클라이언트에게 에러 메세지를 전달하는 기능을 추가했다.
ErrorResponse 객체를 gson 을 사용해 JSON 형태로 변환하여 HttpServletResponse 출력 스트림을 통해 클라이언트로 전달한다.
@Slf4j
public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.error("# Authentication failed: {}", exception.getMessage());
sendErrorResponse(response);
}
private void sendErrorResponse(HttpServletResponse response) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}
2. Gson 라이브러리 추가: Gson을 사용하기 위해서는 라이브러리 추가를 해주어야한다.
Gson은 Google에서 개발한 Java용 JSON 라이브러리로 객체를 직렬화 / 역질렬화 하는 기능을 제공한다.
dependencies {
...
implementation 'com.google.code.gson:gson:2.8.7'
}
3. SecurityConfiguration 설정: 작성한 AuthenticationFailureHandler를 설정파일에 추가한다.
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
jwtAuthenticationFilter.setFilterProcessesUrl("/login");
// Handler 추가
jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer);
builder.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}
}
[결과 확인]
처음에는 기본값 형태(좌)로 응답이 왔었으나, handler 적용 후에는 오른쪽과 같이 지정한 형태로 응답이 도착했다.
필요에 따라 해당 내용을 수정하여 활용할 수 있다.
로그도 잘 남겨져있는것을 확인할 수 있다.
[인증 실패 처리]
1. JwtVerificationFilter: 지난번에 작성했던 JWT를 검증하는 필터이다.
JWT 검증과정에서 유효시간 초과, 부적절한 서명 등 예외가 발생하면
해당 예외를 catch 해서 HttpServletRequest의 Attribute로 해당 예외를 "exception"이라는 이름으로 저장해두도록 했다.
public class JwtVerificationFilter extends OncePerRequestFilter {
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try{
Map<String, Object> claims = verifyJws(request);
setAuthenticationContext(claims);
} catch (SignatureException se) {
request.setAttribute("exception", se);
} catch (ExpiredJwtException ee) {
request.setAttribute("exception", ee);
} catch (Exception e) {
request.setAttribute("exception", e);
} // 에러가 발생하면 request attribute로 "exception"을 등록한다.
filterChain.doFilter(request, response);
}
2. MemberAuthenticationEntryPoint: AuthenticationEntrypoint를 구현하는 클래스이다.
AuthenticationEntryPoint는 인증이 필요한 리소스에 접근할 때 호출되는 인터페이스이다.
AuthenticationException이 발생한 경우에 처리해주는 핸들러 역할을 하도록 설정할 수 있다.
commence() 메서드를 재정의하면 원하는 동작을 지정할 수 있다. (JSON 형태의 에러 응답 전송, 로그인페이지 리다이렉트 등)
@Component
@Slf4j
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Gson gson = new Gson();
private static final ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("# Unauthorized error happened: {}", request.getAttribute("exception"));
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
// MemberAuthenticationFailureHandler의 아래 코드와 같은 역할. errorResponse를 직렬화 한다.
// response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
try (Writer writer = response.getWriter()) {
gson.toJson(errorResponse, writer);
}
}
}
3. SecurityConfiguration: 작성한 EntryPoint를 설정파일에 적용한다.
@Configuration
public class SecurityConfiguration implements WebMvcConfigurer {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint())
.and()
...
return http.build();
}
[결과 확인]
잘못된 토큰을 넣어 보냈을때 처음에는 기본값 형태(좌)로 응답이 왔었으나,
EntryPoint 적용 후에는 오른쪽과 같이 지정한 형태로 응답이 도착했다. 필요에 따라 내용은 변경해서 사용할 수 있다.
참고자료
Restful API 구현을 위한 Spring Security(AuthenticationEntryPoint, AccessDeniedHandler)
'부트캠프 개발일기 > Pre-Project' 카테고리의 다른 글
91일차: Pre-Project Day 11 (Mapper/MapStruct 생성안됨) (0) | 2023.06.23 |
---|---|
90일차: Pre-Project Day 10-2 (코드 Merge, CORS 오류 해결, Ngrok) (0) | 2023.06.23 |
89일차: Pre-Project Day 9 (HandlerInterceptor, JWT 정보 가져오기) (0) | 2023.06.21 |
88일차: Pre-Project Day 8-2 (JWT 로그아웃, Redis) (2) | 2023.06.20 |
88일차: Pre-Project Day 8-1 (JWT 검증) (0) | 2023.06.20 |