클라이언트 쪽에서 전송한 username과 password에 대한 인증을 처리하는 JwtAuthenticationFilter(UsernamePassworAuthenticationFilter를 확장)에서 로그인 인증에 성공한 뒤, JWT를 클라이언트 쪽에 응답으로 전달했다고 가정해 봅시다.

이제 클라이언트는 백엔드 애플리케이션의 API 엔드포인트를 이용할 때 마다 백엔드 애플리케이션으로부터 전달 받은 JWT를 header에 포함시켜 request를 전송합니다.

만약 JWT를 검증하는 필터인 JwtVerificationFilter 구현되어 있는 상태라면 백엔드 애플리케이션에서는 이 JwtVerificationFilter를 이용해 클라이언트의 request header 등에 포함된 JWT를 검증하는 처리를 할 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class JwtVerificationFilter extends OncePerRequestFilter {
    ...
    ...
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        // System.out.println("# JwtVerificationFilter");
 
        try {
            Map<String, Object> claims = verifyJws(request);    // (1)
            setAuthenticationToContext(claims);           // (2)
        } catch (SignatureException se) {
            request.setAttribute("exception", se);
        } catch (ExpiredJwtException ee) {
            request.setAttribute("exception", ee);
        } catch (Exception e) {
            request.setAttribute("exception", e);
        }
 
        filterChain.doFilter(request, response);
    }
    ...
    ...
}
코드 1-1 JwtVerificationFilter 코드 일부
cs

코드 1-1은 JwtVerificationFilter의 코드 일부입니다.

먼저 (1)에서 클라언트로부터 전달 받은 JWT를 검증합니다.

검증에 성공하면 (2)에서 처럼 인증된(authenticated) Authentication 객체를 SecurityContext에 저장합니다.

🙄 그런데 만약에 (2)와 같이 Authentication 객체를 SecurityContext에 저장하지 않으면 어떻게 될까요?

SecurityFilterChain에서 아직 처리가 진행되지 않은 즉, 뒤에 남아있는 Filter 어딘가에서 SecurityContext에 저장된 Authentication 객체가 있는지 확인했는데 Authentication 객체가 존재하지 않아서 결국엔 정상적인 요청 프로세스가 진행되지 못하고 클라이언트 쪽에 에러 메시지를 전송하게 됩니다.

따라서 JWT 검증 이후에 SecurityContext에 Authentication 객체를 반드시 저장해 주어야 한다는 사실을 기억해야 합니다.

그런데 이런 생각을 해볼 수 있습니다. 

💡
코드 1-1과 같은 로직에서는 클라이언트 쪽에서 request를 전송할 때 마다 JWT를 검증하고, SecurityContext에 Authentication 객체를 매 번 저장할 텐데 SecurityContext에 Authentication 객체가 매번 저장되어서 쌓여있거나(물론 그런일은 없겠지만), Authentication 객체가 SecurityContext에 남아 있는건 아닐까?

다행히 Spring Security에서는 적절한 시점에 SecurityContext를 사용할 수 없도록 Authentication 객체가 저장되어 있는 SecurityContext 자체를 삭제합니다.

그 적절한 시점은 언제일까요?

클라이언트의 요청을 모두 처리하고 응답을 리턴하는 어느 시점에 더이상 Authentication 객체가 필요 없어질텐데요.

더이상 필요없어진 SecurityContext를 삭제하는 역할을 하는 것이 바로 SecurityContextPersistenceFilter입니다.

SecurityContextPersistenceFilter는 SecurityContext를 저장, 로드, 삭제하는 등의 역할을 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    ...
    ...
 
    public SecurityContextPersistenceFilter() {
        this(new HttpSessionSecurityContextRepository());
    }
 
    public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
        this.repo = repo;
    }
 
    ...
    ...
 
    private void doFilter(HttpServletRequest request, 
                          HttpServletResponse response, 
                          FilterChain chain)
            throws IOException, ServletException {
        ...
        ...
        
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        
        // (1)
        SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
        try {
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            if (contextBeforeChainExecution.getAuthentication() == null) {
                logger.debug("Set SecurityContextHolder to empty SecurityContext");
            }
            else {
                if (this.logger.isDebugEnabled()) {
                    this.logger
                            .debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
                }
            }
            chain.doFilter(holder.getRequest(), holder.getResponse());
        }
        finally {
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            
            // (2)
            SecurityContextHolder.clearContext();
            
            // (3)
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
            this.logger.debug("Cleared SecurityContextHolder to complete request");
        }
    }
 
    public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }
 
}

코드 1-2 SecurityContextPersistenceFilter 코드 일부 
cs

코드 1-2는 SecurityContextPersistenceFilter의 코드 일부입니다.

먼저 JWT가 request header 등에 포함되어 전송되었다고 가정하면 SecurityFilterChain의 Filter 중 하나인 SecurityContextPersistenceFilter에 request가 전달 되었을 때,

(1)과 같이 SecurityContextRepository에서 기존에 저장되어 있는 SecurityContext를 로드하는데 JWT를 이용하는 방식에서는 이 SecurityContext 객체 안에 Authenticaion 객체가 저장되어 있지 않습니다.

이유는 JWT 인증 방식은 Stateless 즉, HttpSession에서 인증된 Authentication을 포함한 SecurityContext를 관리하지 않기 때문인데요.

HttpSession을 사용하지 않는 이유는 아래에서 다시 설명하겠습니다.

 

어쨌든 SecurityFiterChain의 실행이 끝난 시점 즉, 클라이언트의 요청이 Controller를 거쳐서 처리를 다 끝낸 후에 메서드 실행 흐름이 SecurityContextPersistenceFilter로 넘어오게 되면 (2)와 같이 SecurityContext를 비우게 됩니다.

물론 (3)에서 SecurityFilterChain 실행 후에 생성된 contextAfterChainExecution(clear하기 직 전에 인증된 Authentication 객체가 저장되어 있는 SecurityContext)이 SecurityContextRepository에 저장되긴 하는데, 여기서 저장되는 contextAfterChainExecution는 Spring Security가 Session 인증 방식으로 동작할 경우에는 HttpSession에 contextAfterChainExecution을 저장하고, JWT 인증 방식으로 동작할 경우에는 HttpSession에 저장하는 것이 아니라 단순히 this.repo.saveContext(...)와 같이 메서드 호출만 할 뿐이지 내부적으로는 아무 동작도 하지 않습니다.

이렇게 SecurityContextRepositorysaveContext(...)를 호출하지만 아무런 동작도 하지 않는 이유는  JWT 인증 방식을 사용하기 위해 SecurityConfiguration에서 아래와 같은 설정을 추가하기 때문입니다.

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

SessionCreationPolicy.STATELESS는 HttpSession을 사용하지 않겠다는 의미인데, 이 경우 코드 1-2의 (3)에서 사용하는 SecurityContextRepository의 구현 클래스는 HttpSessionSecurityContextRepository가 아니라 NullSecurityContextRepository가 됩니다. NullSecurityContextRepositorysaveContext(...)는 아래와 같습니다.

public final class NullSecurityContextRepository implements SecurityContextRepository {
	...
    ...
    
	@Override
	public void saveContext(SecurityContext context, 
    						HttpServletRequest request, 
                            HttpServletResponse response) {
	}

}

코드를 보면, NullSecurityContextRepository의 saveContext(...)에는 아무런 로직이 없이 그냥 비어있습니다. 😑

 

SecurityContext를 clear 한다는 의미는 무엇일까?

마지막으로 한 가지만 더 이야기 하고 이번 글을 마치도록 하겠습니다.

우리가 코드 1-2의 (2)에서 SecurityContextHolder.clearContext();를 통해 SecurityContext를 비운다(clear)라고 했습니다.

여기서 clear의 의미는 정확하게 무엇일까요?

SecurityContext를 관리하는 관리 주체는 SecurityContextHolder인데, SecurityContextHolder는 SecurityContext를 어떻게 관리할지에 대한 몇 가지 전략(SecurityContextHolderStrategy)을 취할 수 있습니다.

시스템 프로퍼티에 별도의 전략을 설정하지 않을 경우, 디폴트 전략은 ThreadLocalSecurityContextHolderStrategy입니다.

즉, SecurityContext를 ThreadLocal 변수에 저장해서 관리하겠다라는 전략입니다.

이 말은 clearContext()를 호출하면 현재 실행 중인 Thread에서 사용하는 ThreadLocal 변수에 저장된 데이터 즉, SecurityContext 객체를 지우겠다라는 의미입니다.

실제로 ThreadLocalSecurityContextHolderStrategy의 코드를 보면 아래와 같이 구성되어 있습니다.

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

   // (1) SecurityContext를 ThreadLocal 변수에서 관리한다.
   private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

   @Override
   public void clearContext() {
      contextHolder.remove();
   }

   @Override
   public SecurityContext getContext() {
      SecurityContext ctx = contextHolder.get();
      if (ctx == null) {
         ctx = createEmptyContext();
         contextHolder.set(ctx);
      }
      return ctx;
   }

   @Override
   public void setContext(SecurityContext context) {
      Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
      contextHolder.set(context);
   }

   @Override
   public SecurityContext createEmptyContext() {
      return new SecurityContextImpl();
   }

}

코드를 보면 (1)과 같이 ThreadLocal<SecurityContext> 필드 변수를 포함하고 있으며, setContext(SecurityContext context)로 SecurityContext를 ThreadLocal 변수에 저장하고, getContext()로 ThreadLocal 변수에 저장된 SecurityContext를 가지고 옵니다.

그리고 clearContext()ThreadLocal 변수에 저장되어 있는 SecurityContext를 제거하고 있습니다.

 

SecurityContextPersistenceFilter의 역할

추가로 SecurityContextPersistenceFilter의 역할에 대해서 아래와 같이 간단하게 정리하고 글을 마무리 하도록 하겠습니다.

✔️ SecurityFilterChain이 시작하는 시점에.

👉 세션 기반 자격 증명일 경우,

  • HTTP Session에서 SecurityContext를 로드(load)한다.
  • 로드한 SecurityContext를 SecurityContextHolder에 저장한다(hold)

👉 토큰 기반(JWT 등) 자격 증명일 경우,

  • 비어있는 SecurityContext를 로드(load)한다. 즉, 아무일도 하지 않음.
  • 로드한 비어 있는 SecurityContext를 SecurityContextHolder에 저장한다(hold)
  • ⭐⭐ 비어 있으므로 누군가가 채워줘야 한다. ⭐⭐

 

✔️ SecurityFilterChain이 끝나는 시점에.

👉 세션 기반 자격 증명일 경우

  • SecurityContextHolder에 저장된 SecurityContext를 제거(clear)한다.
  • SecurityContext를 HTTP Session에 저장한다.

👉 토큰 기반(JWT 등) 자격 증명일 경우,

  • SecurityContextHolder에 저장된 SecurityContext를 제거(clear)한다.

 

💡 참고로 SecurityContextPersistenceFilter는 Spring Security 5.7 이후로 Deprecated 되었으며, 5.7 버전 이후로는 SecurityContextPersistenceFilter 대신에 SecurityContextHolderFilter를 사용하기를 권장하고 있습니다.

 

 

이 글이 SecurityContext를 이해하는데 도움이 되셨길 바랍니다.

 

+ Recent posts

출처: http://large.tistory.com/23 [Large]