@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);