반응형
서론
사이드프로젝트 문서 작업을 하며 통합 테스트를 하는 도중,
이전부터 처리가 되지 않았던 403 예외쪽 커스터마이징 문제를 해결해야 겠다는 생각을 했다.
프론트 개발자와 협업을 하며 모든 예외에 대한 응답은 공통 규격을 사용하기로 했었기 때문에 이전에 401 예외는
커스터마이징을 통해 적용이 되어있었으나 403은 자꾸만 아래와 같이 응답을 하고 있었다.
본론
우선 Spring Security는 401에러와 403 에러가 날 때 따로 설정한 Handler가 없으면 위와 같은 응답 형식을 띈다고 한다.
나는 이 내용을 잘 몰랐기 때문에 기존 코드에 꼼수를 통해 401은 예외 처리를 했지만, 403을 위해서는 Spring Security 설정이 필요해 보였다. 따라서 401(인증)과 403(인가)의 Handler를 각각 만들고 Spring Security에 설정을 해주었다.
401(인증)용 Handelr Class
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
setResponse(response,"인증에 실패하였습니다.");
}
private void setResponse( HttpServletResponse response, String message) throws IOException {
ApiErrorResponse apiErrorResponse = ApiErrorResponse.builder()
.statusCode(HttpStatus.FORBIDDEN.value())
.result(ApiErrorMessageAndCode.builder()
.message(message)
.build())
.build();
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(apiErrorResponse));
}
}
403(인가)용 Handelr Class
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
setResponse(response,ErrorMessage.NOT_EXIST_TOKEN_INFO_EXCEPTION);
}
private void setResponse( HttpServletResponse response, ErrorMessage errorMessage) throws IOException {
ApiErrorResponse apiErrorResponse = ApiErrorResponse.builder()
.statusCode(errorMessage.getCode())
.result(ApiErrorMessageAndCode.builder()
.errorCode(errorMessage.getErrorCode())
.message(errorMessage.getMsg())
.build())
.build();
response.setStatus(errorMessage.getCode());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(apiErrorResponse));
}
}
이후 Spring Security에 위 파일들을 DI 시킨 후 설정을 해주었다.
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
private final JwtExceptionFilter jwtExceptionFilter;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션을 사용하지 않는다고 설정.
http.httpBasic().disable()
.authorizeRequests() // 요청에 대한 사용 권한 체크
.antMatchers("/test").authenticated() // authenticated : andMatchers의 URL로 요청이 오면 인증이 필요하다고 설정
.antMatchers(HttpMethod.POST,"/resume").authenticated()
.antMatchers(HttpMethod.DELETE,"/resume").authenticated()
.antMatchers(HttpMethod.GET,"/tagged").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN") // antMatchers : 해당 URL 요청시 설정해줌
.antMatchers("/user/**").hasRole("USER")// hasRole : antPatterns URL로 요청이 들어오면 권한을 확인한다.
// .antMatchers(HttpMethod.POST,"/api/v1/board").authenticated() // antPatterns 에 대한 HTTP POST 요청이 인증되어야 함을 말해 준다.
// .antMatchers("/api/v1/comment").authenticated()
.antMatchers("/**").permitAll()// permitAll : 다른 모든 요청들을 인증이나 권한 없이 허용
.and()
.cors()
.and()
.exceptionHandling()
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
// JwtAuthenticationFilter를 UserIdPasswordAuthenticationFilter 전에 넣는다 + 토큰에 저장된 유저정보를 활용하여야 하기 때문에 CustomUserDetailService 클래스를 생성
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), // 필터를 등록함, 파라미터 - 1번째 : 커스텀한 필터링, 2번쨰 : 필터링전 커스텀 필터링 수행
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter,JwtAuthenticationFilter.class); // jwt 에러처리를 위한 필터등록
}
}
설정은 정말 간단했다.
코드에 exceptionHadling()을 추가하고, 그 뒤에 accessDeniedHandler와 authenticationEntryPoint를 추가하여 위에서 생성한 핸들러를 추가해주면 적용이 되었다.
결론
위 코드를 적용시켜 포스트맨을 통해 다시 테스트를 해보니 아래와 같은 커스터마이징된 응답을 얻을 수 있었다.
반응형
'💻Spring' 카테고리의 다른 글
[REST API] GET 메서드에서의 데이터 전달 (0) | 2023.09.25 |
---|---|
Spring Security 사용자 권한에 따른 API 접근 설정하기 (0) | 2023.09.14 |
MySQL , JPA 에서의 Batch Insert 적용기 (0) | 2023.08.08 |
JPA 프록시 객체 equals의 Override (0) | 2023.08.08 |
Spring OAuth 로그인 및 회원가입 RestTemplate > WebClient 리팩토링 (0) | 2023.06.28 |