JWT 검증

이호영·2023년 9월 4일
0

JWT

목록 보기
4/5

들어가기 전...

이전 포스트에서 JWT토큰을 생성하고 쿠키에 담아 클라이언트의 쿠키에 저장하도록 했습니다.
하지만 이 토큰이 우리가 발급한 토큰인지 검증하는 과정이 있어야 하겠죠?
이번 포스트에서는 JWT토큰의 검증을 한번 살펴 보겠습니다.

SecurityConfig

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf()
                .ignoringAntMatchers("/ws/**", "/room") // Add this line to disable CSRF for WebSocket connections.
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                .exceptionHandling().accessDeniedPage("/main/restrict")
                .authenticationEntryPoint(jwtAuthenticationEntryPoint) // JWT 인증 에러 핸들링
                // JWT 필터 추가
                .and()
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

SecurityConfig에서 JWT 필터를 추가 합니다.

jwtAuthenticationEntryPoint

먼저, 인증 오류가 발생했을 때 처리할 JwtAuthenticationEntryPoint 클래스를 작성합니다. 이 클래스는 AuthenticationEntryPoint 인터페이스를 구현합니다.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException {
        String exception = (String) request.getAttribute(JwtProperties.HEADER_STRING);
        String errorCode;

        if (exception != null && exception.equals("토큰 만료.")) {
            errorCode = "토큰 만료.";
            setResponse(response, errorCode);
        }

        if (exception != null && exception.equals("유효 않은 토큰.")) {
            errorCode = "유효 않은 토큰.";
            setResponse(response, errorCode);
        }
    }

    private void setResponse(HttpServletResponse response, String errorCode) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JwtProperties.HEADER_STRING + " : " + errorCode);
    }
}

위 코드에서는 HttpServletRequest의 attribute에서 예외 상황 정보를 가져옵니다. 그리고 그 예외 상황에 따라 적절한 에러 메시지를 설정하고 HTTP 상태 코드로 401(Unauthorized)을 설정하여 클라이언트에게 보냅니다.

JwtRequestFilter

다음으로 요청에서 JWT 토큰을 추출하고 검증하는 필터인 JwtRequestFilter 클래스를 작성합니다.

@RequiredArgsConstructor
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private final RedisService redisService;
    private final JwtTokenProvider jwtTokenProvider; // JwtTokenProvider 추가

    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        String servletPath = request.getServletPath();

        if (isSwaggerPath(servletPath)) {
            filterChain.doFilter(request, response);
            return;
        }

        String jwtHeader = jwtTokenProvider.getTokenFromRequest(request);


        // header 가 정상 비정상 ?
        if (jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
            filterChain.doFilter(request, response);
            return;
        }

        // jwt 토큰을 검증 정상 사용자 확인
        String token = jwtHeader.replace(JwtProperties.TOKEN_PREFIX, "").trim();

        Boolean isTokenRevokedOrExpired = redisService.isTokenExpired(token); // Redis 저장된 토큰 확인

        if (isTokenRevokedOrExpired) { // 전환된 토큰이거나 토큰 만료된 경우
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "전환된 토큰 또는 만료된 토큰입니다.");
            return;
        }


        Long userCode = null;
        try {
            userCode = jwtTokenProvider.getUserIdFromToken(jwtHeader);

        } catch (TokenExpiredException e) {
            e.getStackTrace();
            request.setAttribute(JwtProperties.HEADER_STRING, "토큰 만료");
        } catch (JWTVerificationException e) {
            e.getStackTrace();
            request.setAttribute(JwtProperties.HEADER_STRING, "유효 않는 토큰.");
        }

        request.setAttribute("userCode", userCode);

        filterChain.doFilter(request, response);
    }

    private boolean isSwaggerPath(String path) {
        String[] swaggerPaths = {
                "/swagger-ui/**",
                "/swagger-resources/**",
                "/v3/api-docs/**",
                "/v2/api-docs/**",
                "/webjars/**",
        };

        return Arrays.stream(swaggerPaths).anyMatch(path::startsWith);
    }
}

위 코드에서는 JwtTokenProvider를 이용해 JWT 토큰을 검증하고 사용자 ID를 추출합니다. 토큰이 만료되었거나 유효하지 않은 경우에는 적절한 예외 메시지를 HttpServletRequest의 attribute에 설정합니다.
또한, swagger에 대한 예외 처리를 합니다.

JwtTokenProvider

@Component
public class JwtTokenProvider {

    public String getTokenFromRequest(HttpServletRequest request) {
        String jwtHeader = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (JwtProperties.HEADER_STRING.equals(cookie.getName())) {
                    jwtHeader = cookie.getValue();
                    break;
                }
            }
        }
        return jwtHeader;
    }

    public Long getUserIdFromToken(String token) {
        try {
            return JWT.require(Algorithm.HMAC512(JwtProperties.SECRET))
                    .build()
                    .verify(token.replace(JwtProperties.TOKEN_PREFIX, "").trim())
                    .getClaim("id")
                    .asLong();
        } catch (JWTVerificationException e) {
            return null;
        }
    }
}

위 클래스에서는 getTokenFromRequest를 통해 요청에서 쿠키속 JWT를 파싱합니다.

마무리

이렇게 해서 Spring Security와 JWT를 이용해 인증 정보를 검증하는 방법을 알아보았습니다. 이 방식은 웹 애플리케이션에서 사용자 세션을 관리하고, 보안성 높은 인증 절차를 구현하는 데 활용될 수 있습니다.

0개의 댓글