Spring-Security

jwt 사용할 때, securitycontext는 어떻게, 어디에 저장되나?

neal89 2025. 5. 29. 09:02

Spring Security에서 기본적으로는 인증 정보(SecurityContext)를 세션(HttpSession) 에 저장합니다.
하지만 JWT 기반 인증에서는 보통 세션을 사용하지 않습니다 (stateless).

이때는 다음과 같은 방식으로 처리됩니다:


✅ JWT 인증 흐름에서의 SecurityContext 저장 처리 방식

1. SecurityContextRepository 전략을 null 또는 custom으로 설정

  • 세션을 통한 저장 방식을 완전히 제거하고,
  • 매 요청(request)마다 JWT를 파싱해서 인증 정보를 구성합니다.

2. JWT 필터에서 직접 SecurityContext에 저장

  • 사용자가 요청을 보낼 때, JWT를 헤더(보통 Authorization)에 담아 보냅니다.
  • 커스텀 필터(예: JwtAuthenticationFilter)에서:
    • JWT를 검증하고,
    • Authentication 객체를 만들고,
    • SecurityContextHolder.getContext().setAuthentication(authentication) 으로 현재 쓰레드에 보관합니다.
  • 이 정보는 해당 요청의 스레드 안에서만 유효합니다.

3. 요청이 끝나면 SecurityContext는 사라짐

  • 세션이 없기 때문에, SecurityContext는 요청이 끝나면 버려집니다.
  • 다음 요청이 오면 다시 JWT를 파싱해서 SecurityContext를 다시 구성해야 합니다.

Answer in English

In JWT-based Spring Security applications, where sessions are disabled (i.e., stateless configuration), the SecurityContext is not stored in the session. Instead, here's what happens:

  1. A custom filter (e.g., JwtAuthenticationFilter) intercepts every request.
  2. It extracts and verifies the JWT from the request header.
  3. It builds an Authentication object and stores it using:
    SecurityContextHolder.getContext().setAuthentication(authentication);
  4. This context is thread-local and exists only for the duration of the request.
  5. Since there's no session, the context is not persisted and must be recreated on every request.

This is the standard flow in stateless authentication setups like REST APIs using JWT.


JWTFilter

package teo.springjwt.common.jwt;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import teo.springjwt.user.UserEntity;
import teo.springjwt.user.UserRole;
import teo.springjwt.user.dto.CustomUserDetails;

public class JWTFilter extends OncePerRequestFilter {

  private final JWTUtil jwtUtil;

  public JWTFilter(JWTUtil jwtUtil) {

    this.jwtUtil = jwtUtil;
  }


  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException, IOException {

    //request에서 Authorization 헤더를 찾음
    String authorization= request.getHeader("Authorization");

    System.out.println("authorization = " + authorization);

    //Authorization 헤더 검증
    if (authorization == null || !authorization.startsWith("Bearer ")) {

      System.out.println("token null");
      filterChain.doFilter(request, response);

      //조건이 해당되면 메소드 종료 (필수)
      return;
    }

    System.out.println("authorization now");
    //Bearer 부분 제거 후 순수 토큰만 획득
    String token = authorization.split(" ")[1];

    // --- JWT 자체 유효성 검증 로직 추가 (핵심 개선 부분) ---
    try {
      if (!jwtUtil.validateToken(token)) { // validateToken 메서드에서 서명, 구조, 만료 등을 모두 검증
        System.out.println("token validation failed (invalid signature, malformed, or expired)");
        // 이 경우 401 Unauthorized 또는 다른 적절한 HTTP 상태 코드와 메시지를 반환하는 것이 좋습니다.
        // 하지만 현재는 필터 체인을 계속 진행하지 않고 반환.
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
        return;
      }
    } catch (ExpiredJwtException e) {
      // 만료된 토큰에 대한 특별 처리 (예: Refresh Token 흐름 유도)
      System.out.println("token expired (caught in filter): " + e.getMessage());
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
      return;
    } catch (JwtException e) { // 그 외 JWT 관련 모든 예외 (Signature, Malformed 등)
      System.out.println("invalid JWT token (caught in filter): " + e.getMessage());
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
      return;
    }
    // --- JWT 유효성 검증 로직 끝 ---

    //토큰에서 username과 role 획득
    String username = jwtUtil.getUsername(token);

    // jwt에서 enum으로 바로 변환 못함. 그래서 null 에러가 발생.
    /*
    Error extracting role from token: Cannot convert existing claim value of type 'class java.lang.String' to desired type 'class teo.springjwt.user.UserRole'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more complex is expected to be already converted to your desired type by the JSON Deserializer implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion configuration via the JwtParserBuilder.deserializer() method. See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types
    * */
    String roleString =jwtUtil.getRole(token);

    //userEntity를 생성하여 값 set
    UserEntity userEntity = new UserEntity();
    userEntity.setUsername(username);
    // 비밀번호를 null 로 설정해둔다.
    userEntity.setPassword(null);
    userEntity.setRole(UserRole.valueOf(roleString));

    //UserDetails에 회원 정보 객체 담기
    CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

    //스프링 시큐리티 인증 토큰 생성
    // jwt를 사용하고 있기 때문에, credential 쪽은 null
    Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

    // 스프링 시큐리티의 인증 정보를 현재 실행 스레드에 등록하는 표준적인 방법
    // 이 자체가 세션을 만든다는 뜻은 아니다.
    SecurityContextHolder.getContext().setAuthentication(authToken);

    filterChain.doFilter(request, response);
  }
}