
@RequiredArgsConstructor
public class JwtVerificationFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
private final JwtAuthorityUtils authorityUtils;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
try {
Map<String, Object> claims = verifyJws(request);
setAuthenticationToContext(claims);
} catch (SignatureException se) {
request.setAttribute("exception", se);
} catch (ExpiredJwtException ee) {
request.setAttribute("exception", ee);
} catch (Exception e) {
request.setAttribute("exception", e);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(
HttpServletRequest request
) throws ServletException {
String authorization = request.getHeader("Authorization");
return authorization == null || !authorization.startsWith("Bearer ");
}
private Map<String, Object> verifyJws(HttpServletRequest request) {
String jws = request.getHeader("Authorization").replace("Bearer ", "");
String base64SecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
return jwtTokenizer.getClaims(jws, base64SecretKey).getBody();
}
private void setAuthenticationToContext(Map<String ,Object> claims) {
String username = (String) claims.get("username");
List<GrantedAuthority> roles = authorityUtils.createAuthorities((List<String>) claims.get("roles"));
Authentication authentication =
new UsernamePasswordAuthenticationToken(username, null, roles);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
OncePerRequestFilter를 확장하여 하나의 Request 당 한번만 실행되는 SecurityFilter를 구현하였다.JwtTokenizer를 주입받는다.Authentication을 채울 사용자 권한을 생성하기 위해 CustomAuthorityUtils을 주입 받는다.verifyJws() 메소드는 JWT를 검증하기위한 메소드다.replace()로 토큰외 부분을 제거한다.setAuthenticationToContext()에서는 Authentication 객체를 SecurityContext에 저장한다.JwtVerificationFilter의 doFilterInternal() 메소드 내부에서 try-catch 문으로 예외를 잡아 request로 넘겨주고 있다.
넘겨 받은 AuthenticationException 예외들을 처리하기위한 AuthenticationEntryPoint와 인증 성공후 해당 리소스에 권한이 없을때 발생하는 AccessDeniedException를 처리하기 위한 AccessDeniedHandler를 구현해 보자.
에러에 대한 응답 DTO는 ErrorResponse클래스로 따로 정의 되어 있으며, 코드는 생략하겠다. 우선 ErrorResponse를 출력 스트림으로 생성해주는 ErrorResponder 클래스를 정의했다.
public class ErrorResponder {
public static void sendErrorResponse(HttpServletResponse response, HttpStatus status) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(status.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}
AuthenticationException 예외들을 처리하기 위해 AuthenticationEntryPoint 을 구현한 클래스다
@Slf4j
@Component
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Exception exception = (Exception) request.getAttribute("exception");
ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED);
logExceptionMessage(authException, exception);
}
private void logExceptionMessage(AuthenticationException authException, Exception exception) {
String message = exception != null ? exception.getMessage() : authException.getMessage();
log.warn("Unauthorized error happened: {}", message);
}
}
commence()는 AuthenticationException이 발생할 경우 호출된다.
@Slf4j
@Component
public class MemberAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN);
log.warn("Forbidden error happened: {}", accessDeniedException.getMessage());
}
}
AccessDeniedHandler을 구현한 클래스다. handle()메소드는 요청한 리소스에 대한 사용자의 권한이 없을 경우 AccessDeniedException이 발생하여 호출된다.
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenizer jwtTokenizer;
private final JwtAuthorityUtils authorityUtils;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(Customizer.withDefaults())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
// 예외처리 handler 추가
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint())
.accessDeniedHandler(new MemberAccessDeniedHandler())
.and()
.apply(new CustomFilterConfig())
.and()
.authorizeRequests(auth -> auth.anyRequest().permitAll());
// .....
//......
public class CustomFilterConfig extends AbstractHttpConfigurer<CustomFilterConfig, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
// 검증 필터 추가
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);
builder.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}
}
}
}
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint())
.accessDeniedHandler(new MemberAccessDeniedHandler())
CustomFilterConfig의 configure()에 권한 검증 필터를 추가해 주고 아래와 같이 필터가 적용될 순서를 지정해 준다. builder.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);