이번 포스트는 Custom한 예외처리 (StatusCode)를 이용하여 JWT토큰에 관련된 예외처리를 작성한 부분을 담으려 한다.
ExceptionTranslationFilter
ExceptionTranslationFilter는 2가지 종류의 예외를 처리
FilterSecurityInterceptor(보안필터중 제일 마지막에 위치)가 발생시킴
여기에서 인증,인가예외를 Throws함
가. AuthenticationException - 인증예외처리
- AuthenticationEntryPoint 호출 : 로그인페이지 이동, 401오류 코드 전달
- 인증예외가 발생하기 전의 요청 정보를 저장
- RequestCache - 사용자의 이전 요청정보를 세션에 저장하고, 이를 꺼내오는 캐시 메커니즘
- SavedRequest - 사용자가 요청했던 request파라미터 값들, 그 당시의 헤더값들 등이 저장
나. AccessDeniedException - 인가예외처리
- AccessDeniedHandler에서 예외처리 하도록 제공
AuthenticationEntryPoint란 ?
인증 처리 과정에서 예외가 발생한 경우 예외를 핸들링하는 인터페이스
AuthenticationException(인증되지 않은 요청)인 경우 AuthenticationEntryPoint를 사용하여 처리
AuthenticationEntryPoint 인터페이스 구현체 생성
SecurityConfig에서 .exceptionHandling() 메소드에 등록
전체 코드
1. JwtUtil
// 기능 : JWT 유틸
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final UserDetailsServiceImpl userDetailsService;
public static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰 생성
public String createToken(String loginId){
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(loginId)
// .claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
public String resolveToken(HttpServletRequest request){
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)){
return bearerToken.substring(7);
}
return null;
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
public Authentication createAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
2. JwtAuthFillter
// 기능 : JWT 필터
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
public final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
if(token != null) {
if(!jwtUtil.validateToken(token)){
response.setStatus(StatusCode.INVALID_TOKEN.getHttpStatus().value());
response.setContentType("application/json; charset=UTF-8");
try {
String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto(StatusCode.INVALID_TOKEN));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
return;
}
// 3. 토큰이 유효하다면 토큰에서 정보를 가져와 Authentication 에 세팅
Claims info = jwtUtil.getUserInfoFromToken(token);
setAuthentication(info.getSubject());
}
// 4. 다음 필터로 넘어간다
filterChain.doFilter(request, response);
}
public void setAuthentication(String loginId) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = jwtUtil.createAuthentication(loginId);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
public void jwtExceptionHandler(HttpServletResponse response, String message, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
try {
String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto<>(StatusCode.INVALID_TOKEN));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
3. SecurityConfig
// 기능 : Spring Security 사용에 필요한 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.httpBasic().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/**").permitAll()
.antMatchers("/ws-stomp").permitAll()
.antMatchers("/signal/**").permitAll()
// .antMatchers("/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthFilter(jwtUtil),
UsernamePasswordAuthenticationFilter.class);
http.cors();
// Spring Security에서 발생하는 예외를 커스텀 핸들링
http.exceptionHandling()
// 인증부분 (여기서만 사용하기 때문에 익명함수 람다식 이용)
.authenticationEntryPoint((request, response, authException) -> {
securityExceptionResponse(response, INVALID_TOKEN);
});
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("https://S3 버켓이름.s3-website.ap-northeast-2.amazonaws.com");
config.addAllowedOrigin("https://프론트엔드 서버주소.cloudfront.net");
config.addExposedHeader(JwtUtil.AUTHORIZATION_HEADER);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.validateAllowCredentials();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// 예외 응답처리 메소드
public void securityExceptionResponse(HttpServletResponse response, StatusCode statusCode) throws IOException {
response.setStatus(statusCode.getHttpStatus().value());
response.setHeader("content-type", "application/json; charset=UTF-8");
String json = new ObjectMapper().writeValueAsString(new GlobalResponseDto(statusCode));
response.getWriter().write(json);
response.getWriter().flush();
response.getWriter().close();
}
}
[ 참고 자료 ]
[SpringSecurity] 스프링시큐리티 - 인증인가 예외발생시 처리 (ExceptionTraslationFilter)
[SpringSecurity] 스프링시큐리티 - 인증인가 예외발생시 처리 (ExceptionTraslationFilter) 1. ExceptionTranslationFilter는 2가지 종류의 예외를 처리 --> FilterSecurityInterceptor(보안필터중 제일 마지막에 위치)가 발생
fenderist.tistory.com
AuthenticationEntryPoint
개인 공부 목적으로 작성한 포스팅입니다. 아래 출처를 참고하여 작성하였습니다. 1. AuthenticationEntryPoint란 ? 인증 처리 과정에서 예외가 발생한 경우 예외를 핸들링하는 인터페이스입니다. e.g. Au
batory.tistory.com