@ControllerAdivce
로 예외 처리할 수 없습니다.@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // rest api : csrf, httpBasic, formLogin 미사용
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // jwt 인증 : session 미사용
.authorizeHttpRequests((authz) -> authz // 권한 설정
.requestMatchers("/auth/**").permitAll() // 로그인, 로그아웃 관련
.requestMatchers("/register/**").permitAll() // 회원가입 관련
.requestMatchers("/swagger-ui/**", "/v3/**").permitAll() // api 명세 관련
.requestMatchers("/admin/**").hasRole("ADMIN") // admin 관련
.anyRequest().authenticated()
)
.exceptionHandling(authenticationManager -> authenticationManager
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler()))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);
return http.build();
}
}
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("UTF-8");
final Map<String, Object> body = new HashMap<>();
body.put("status", ErrorCode.ANONYMOUS_USER.getStatus());
body.put("error", ErrorCode.ANONYMOUS_USER.getError());
body.put("message", "로그인이 필요합니다.");
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(body));
}
}
AuthenticationEntryPoint는 인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때, 예외를 핸들링 할 수 있도록 도와준다. Spring Security의 기본 설정으로는 HttpStatus 401과 함께 스프링의 기본 오류페이지를 보여준다. 이를 커스텀하게 사용하기 위해 구현하였다.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException{
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setCharacterEncoding("UTF-8");
final Map<String, Object> body = new HashMap<>();
body.put("status", ErrorCode.ACCESS_DENIED.getStatus());
body.put("error", ErrorCode.ACCESS_DENIED.getError());
body.put("message", "접근 권한이 없습니다.");
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(body));
}
}
인증은 완료되었으나 요청에 대한 권한을 가지고 있지 않은 사용자가 엔드포인트에 접근하려고 할 때, 예외를 핸들링 할 수 있도록 도와준다. Spring Security의 기본 설정으로는 403 Forbidden 오류과 함께 스프링의 기본 오류 페이지를 보여준다. 이를 JSON 형태로 응답하기 위해 구현하였다.
@Component
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
chain.doFilter(request, response);
} catch (JwtException ex) {
setErrorResponse(request, response, ex);
}
}
public void setErrorResponse(HttpServletRequest request, HttpServletResponse response, Throwable ex) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map<String, Object> body = new HashMap<>();
body.put("status", ErrorCode.JWT_FAILURE.getStatus());
body.put("error", ErrorCode.JWT_FAILURE.getError());
body.put("message", ex.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
인증 오류가 아닌, JWT 관련 오류는 이 필터에서 잡아내며, 이를 통해 JWT 만료 에러와 인증 에러를 따로 잡아낼 수 있다.