본문 바로가기

내배캠/TIL

JWT, Filter

JWT와 Session

HTTP stateless, connectionless

→ HTTP의 요청들은 서로 독립적이다!

 

만약 naver.com에 3번 요청을 했다면, 이게 몇 번째인지 알 수 있을까? ⇒ NO!

 

상태를 갖지 않는다는 것은 ‘데이터가 없다는 것’

아무런 데이터가 없기 때문에 이 요청이 이 요청인지 저 요청이 저 요청인지 알 수 없음.

 

근데 우리는 분명 로그인이 가능하다.

로그인이 되었다는 것은 분명 상태(데이터)를 가지고 있다는 뜻.

즉, 클라이언트(유저)나 서버 둘 중 하나는 데이터를 갖고 있어야 한다.

 

누가? → 둘 다 갖고 있음.

  • JWT : 클라이언트가 상태를 갖고 있음
  • Session : 서버가 상태를 갖고 있음

클라이언트와 서버 둘 다 상태(데이터)를 갖고 있을 수 있다.

 

 

로그인 상태일 때,

  • JWT → 클라이언트에서는 JWT(유저 정보)를 갖고 있고 이를 HTTP 요청마다 같이 보내서 항상 자신이 누구인지 알려 HTTP 요청들 간의 독립성을 극복하고,
  • Session → 서버에서는 서버에 Session(유저 정보)를 갖고 있고 클라이언트는 Session Key를 갖고 이를 HTTP 요청마다 같이 보내 HTTP 요청들 간 독립성을 극복.

 

그럼 로그아웃은 어떻게?

  • JWT
  1. 클라이언트에서 JWT를 삭제한다.
  • Session
  1. 클라이언트에서 session key를 삭제한다.
  2. 서버에서 session 데이터를 삭제한다.

(둘 중 하나라도 삭제하면 로그아웃)

 

 

모든 요청마다 필요한 정보를 모두 담아 보내야 하기 때문에 JWT는 Session Key보다 더 용량이 많을 수 밖에 없다.

 

 

JWT의 특징을 한 단어로? ⇒ stateless

 

근데 JWT 안에 정보가 담겨있다면서? → 무상태는 서버 관점. 주체가 서버!!

 

그럼 Session의 특징은? ⇒ stateful

 

 

 

Filter

Client의 요청이 왔을 때 서버의 가장 앞에서 요청을 Filtering하는 용도로 쓰는 것!

 

 

Argument Resolver

JWT에서 꺼낸 정보를 컨트롤러에 객체 지향적으로 전달 가능.

 

 

 

Filter 생성

@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;
    private final Pattern authPattern = Pattern.compile("^/v\\d+/auth.*");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String url = httpRequest.getRequestURI();

        // `/v{숫자}/auth`로 시작하는 URL은 필터를 통과하지 않도록 설정
        if (authPattern.matcher(url).matches()) {
            chain.doFilter(request, response);
            return;
        }

        // NOTE: 위의 방법이 이해가 어려운 분은 이런 방법을 사용하셔도 좋습니다.
//        if (url.startsWith("/v1/auth") || url.startsWith("/v2/auth")) {
//            chain.doFilter(request, response);
//            return;
//        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) {
            // 토큰이 없는 경우 400을 반환합니다.
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {
            // JWT 유효성 검사와 claims 추출
            Claims claims = jwtUtil.extractClaims(jwt);

            // 사용자 정보를 ArgumentResolver 로 넘기기 위해 HttpServletRequest 에 세팅
            httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
            httpRequest.setAttribute("email", claims.get("email", String.class));

            chain.doFilter(request, response);
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
        } catch (Exception e) {
            log.error("JWT 토큰 검증 중 오류가 발생했습니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰 검증 중 오류가 발생했습니다.");
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

 

Filter 등록

@Configuration
@RequiredArgsConstructor
public class FilterConfig {

    private final JwtUtil jwtUtil;

    // Filter 등록
    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter(jwtUtil));
        registrationBean.addUrlPatterns("/*");

        return registrationBean;
    }
}

 

Argument Resolver 생성

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {

    // @Auth 어노테이션이 있는지 확인
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(Auth.class) != null;
    }

    // AuthUser 객체를 생성하여 반환
    @Override
    public Object resolveArgument(
            @Nullable MethodParameter parameter,
            @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            @Nullable WebDataBinderFactory binderFactory
    ) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        // JwtFilter 에서 set 한 userId, email 값을 가져옴
        Long userId = (Long) request.getAttribute("userId");
        String email = (String) request.getAttribute("email");

        return new AuthUser(userId, email);
    }
}

 

Argument Resolver 등록

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    // ArgumentResolver 등록
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthUserArgumentResolver());
    }
}

'내배캠 > TIL' 카테고리의 다른 글

JPA의 N+1 문제 해결하기  (0) 2024.09.06
데이터 베이스 정규화  (1) 2024.09.05
RDBMS vs NoSQL  (0) 2024.09.03
[스프링] 예외 처리  (0) 2024.08.29
스프링 인터셉터  (0) 2024.08.29