JWT와 Session
HTTP stateless, connectionless
→ HTTP의 요청들은 서로 독립적이다!
만약 naver.com에 3번 요청을 했다면, 이게 몇 번째인지 알 수 있을까? ⇒ NO!
상태를 갖지 않는다는 것은 ‘데이터가 없다는 것’
아무런 데이터가 없기 때문에 이 요청이 이 요청인지 저 요청이 저 요청인지 알 수 없음.
근데 우리는 분명 로그인이 가능하다.
로그인이 되었다는 것은 분명 상태(데이터)를 가지고 있다는 뜻.
즉, 클라이언트(유저)나 서버 둘 중 하나는 데이터를 갖고 있어야 한다.
누가? → 둘 다 갖고 있음.
- JWT : 클라이언트가 상태를 갖고 있음
- Session : 서버가 상태를 갖고 있음
클라이언트와 서버 둘 다 상태(데이터)를 갖고 있을 수 있다.
즉 로그인 상태일 때,
- JWT → 클라이언트에서는 JWT(유저 정보)를 갖고 있고 이를 HTTP 요청마다 같이 보내서 항상 자신이 누구인지 알려 HTTP 요청들 간의 독립성을 극복하고,
- Session → 서버에서는 서버에 Session(유저 정보)를 갖고 있고 클라이언트는 Session Key를 갖고 이를 HTTP 요청마다 같이 보내 HTTP 요청들 간 독립성을 극복.
그럼 로그아웃은 어떻게?
- JWT
- 클라이언트에서 JWT를 삭제한다.
- Session
- 클라이언트에서 session key를 삭제한다.
- 서버에서 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 |