이번 포스트는 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

 

+ Recent posts