이전 포스트에서 JWT토큰을 생성하고 쿠키에 담아 클라이언트의 쿠키에 저장하도록 했습니다.
하지만 이 토큰이 우리가 발급한 토큰인지 검증하는 과정이 있어야 하겠죠?
이번 포스트에서는 JWT토큰의 검증을 한번 살펴 보겠습니다.
@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 클래스를 작성합니다. 이 클래스는 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)을 설정하여 클라이언트에게 보냅니다.
다음으로 요청에서 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에 대한 예외 처리를 합니다.
@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를 이용해 인증 정보를 검증하는 방법을 알아보았습니다. 이 방식은 웹 애플리케이션에서 사용자 세션을 관리하고, 보안성 높은 인증 절차를 구현하는 데 활용될 수 있습니다.