Spring Security 기반의 애플리케이션에 아이디/패스워드 로그인 인증 후, 전달 받은 JWT를 이용해 서버 측 리소스에 접근한다고 가정해 봅시다.

이 경우, 매 요청마다 JWT를 request header에 포함해서 요청 전송을 할텐데요.

이 JWT를 JwtAuthorizationFilter가 검증할 경우, 두 가지 케이스를 테스트 해 보았습니다.

 

JwtAuthorizationFilter에서 인증이나 접근 권한 등의 예외를 try ~ catch로 잡는 경우

public class JwtVerificationFilter extends OncePerRequestFilter {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer,
                                 CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
       // JWT 검증 시, 발생하는 예외를 try ~ catch 문으로 catch 하는 경우
       try {
            Map<String, Object> claims = verifyJws(request);
            setAuthenticationToContext(claims);
        } catch (Exception e) {
            request.setAttribute("exception", e);
        }

        filterChain.doFilter(request, response);
    }
    
    ...
    ...
}
  • SecurityFilterChain 동작이 ExceptionTranslationFilter 까지 넘어 간다.
  • ExceptionTranslationFilter에서 다시 AuthorizationFilter로 동작이 넘어간다.
  • AuthorizationDecision.isGranted()이기 때문에 AccessDeniedException이 throw된다.
  • ExceptionTranslationFilter에서 예외를 catch 한다.
  • ExceptionTranslationFilter 내부에서 AccessDeniedException을 처리하는 로직을 수행한다.
    • handleAccessDeniedException()
      • 인증 여부를 체크해서 인증이 되지 않았으면 AuthenticationEntryPoint를 이용해 예외를 처리한다.
      • 인증은 된 상태이고 권한이 적절하지 않다면  AccessDeniedHandler를 이용해 예외를 처리한다.

 

JwtAuthorizationFilter에서 인증이나 접근 권한 등의 예외를 try ~ catch로 잡지 않는 경우

public class JwtVerificationFilter extends OncePerRequestFilter {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer,
                                 CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // JWT 검증 시, try ~ catch 문으로 catch 하지 않는 경우
        Map<String, Object> claims = verifyJws(request);

        filterChain.doFilter(request, response);
    }

    ...
    ...
}
  • 예외가 throw 되어서 톰캣 같은 서블릿 컨테이너 레벨까지 전파된다.
  • `/error` URL로 리다이렉트 된다.
  • Spring Framework의 BasicErrorController의 error() 핸들러 메서드가 `/error` 요청을 수신한다.
  • 디폴트 에러 메시지를 JSON 형태로 리턴한다.

1️⃣ OAuth2LoginAuthenticationFilter

  • OAuth 2 인증을 처리하기 위한 Filter

2️⃣ CommonOAuth2Provider

  • 신뢰할 만한 OAuth 2 Provider 목록 제공
  • 실질적으로 각 Provider에게 넘겨줘야 되는 Client 정보를 포함한 ClientRegistration 객체를 생성한다.

3️⃣ ClientRegistration

  • 각 OAuth 2 Provider 사용을 위해 Client에게 필요한 등록 정보
public enum CommonOAuth2Provider {

	GOOGLE {

		@Override
		public Builder getBuilder(String registrationId) {
			ClientRegistration.Builder builder = getBuilder(registrationId,
					ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL);
      // Provider가 제공할 Resource 범위
			builder.scope("openid", "profile", "email");

      // Client가 OAuth 2 인증을 위해 Redirect하는 URI(구글의 로그인 인증 화면)
			builder.authorizationUri("<https://accounts.google.com/o/oauth2/v2/auth>");

      // Client가 Access Token과 Refresh Token을 얻기 위해 호출하는 URI
			builder.tokenUri("<https://www.googleapis.com/oauth2/v4/token>");

      // JWT 검증용 Key Set을 조회할 수 있는 URI
			builder.jwkSetUri("<https://www.googleapis.com/oauth2/v3/certs>");

      // Access Token 발행자 정보 조회 URI
			builder.issuerUri("<https://accounts.google.com>");

      // Resource Owner의 User Info를 조회하기 위한 URI
			builder.userInfoUri("<https://www.googleapis.com/oauth2/v3/userinfo>");

      // Resource Owner의 이름에 접근하기 위한 Attribute Name
			builder.userNameAttributeName(IdTokenClaimNames.SUB);

      // Client Name. Provider를 구분하는 용도로 사용
			builder.clientName("Google");
			return builder;
		}

	},
  
  ...
  ...

}

 

4️⃣ OAuth2AuthorizedClientService

  • OAuth 2로 인증된 Client(OAuth2AuthorizedClient)를 관리하는 서비스 클래스
  • OAuth2AuthorizedClientService 인터페이스의 구현 클래스
    • InMemoryOAuth2AuthorizedClientService
    • JdbcOAuth2AuthorizedClientService


5️⃣ ClientRegistrationRepository

  • OAuth 2로 인증된 Client(OAuth2AuthorizedClient)를 저장하는 역할을 한다.
    • ClientRegistrationRepository 인터페이스의 구현 클래스
      • InMemoryClientRegistrationRepository
  • OAuth2AuthorizedClientService가 ClientRegistrationRepository를 사용해 OAuth2AuthorizedClient 를 저장한다.



6️⃣ OAuth2AuthorizedClientManager

  • OAuth2AuthorizedClient 를 관리하는 관리자 역할을 한다.



7️⃣ OAuth2AuthorizedClientProvider

  • Authorization Grant 유형을 설정할 수 있는 인터페이스
    • Authorization Code
    • Implicit Grant Type
    • Client Credentials
    • Resource Owner Password Credentials

Spring Security에서 JWT를 발급 받는 방법은 크게 두 가지로 볼 수 있습니다.

첫째는 구글 검색을 통해서도 흔히 볼 수 있는 Controller를 이용해서 아이디/패스워드 로그인 후, JWT를 발급 받는 방식이고, 두 번째는 Spring Security에서 지원하는 Security Filter를 확장하는 방법입니다.

Controller를 이용하는 것이 안좋은 방법이라고 보기는 힘들지만 그래도 Spring Security라는 보안 프레임워크를 사용하는 개발자 입장에서는 Controller까지 request가 전달되기 전에 Security Filter 레벨에서 인증을 처리하는 것이 조금 더 자연스럽지 않을까하는 생각을 해보게 됩니다.

이번 글에는 구체적인 코드는 없지만 JWT를 이용해 어떤식으로 인증이 처리 되는지 그리고 JWT를 이용해서 클라이언트의 자격을 검증하는 프로세스가 어떻게 진행 되는지를 그림으로만 표현하도록 하겠습니다.

이 글에 있는 그림을 보고 CSR(Client Side Rendering) 방식의 애플리케이션에서 JWT를 이용해 인증 및 클라이언트의 자격 검증 로세스를 어떻게 코드로 구현할 수 있는지에 대한 인사이트를 얻어갈 수 있기를 바래봅니다.

 

JWT 인증 프로세스

[그림 1-1] JWT 인증 프로세스

[그림 1-1]의 JWT 인증 프로세스는 다음과 같습니다.

  • 클라이언트가 최초 로그인 인증을 위해 username(이메일 등의 로그인 아이디)과 password를 request body에 실어서 전송합니다.
    • 백엔드 애플리케이션 쪽으로 전송된 username과 password는 JwtAuthenticationFilter에 전달되어 JwtAuthenticationFilter에서부터 클라이언트에 대한 인증 프로세스가 시작됩니다.
    • JwtAuthenticationFilterUsernamePasswordAuthenticationFilter를 확장해서 구현하면 됩니다.
  • JwtAuthenticationFilter에서 AuthenticationManager의 구현 클래스인 ProdiverManager를 거쳐 인증되지 않은(unauthenticated) Authentication 객체가 AuthenticationProvider에게 전달됩니다.
  • AuthenticationProvider는 인증되지 않은(unauthenticated) Authentication 객체에 포함된 username을 UserDetailsService의 구현 클래스인 MemberDetailsService에게 전달합니다.
    • loadUserByUsername(String username)을 이용.
  • UserDetailsService의 구현 클래스인 MemberDetailsService는 데이터베이스에서 사용자의 Credential(암호화 된 패스워드)을 조회한 뒤 조회한 Credential을 포함한 UserDetailsAuthenticationProvider에게 리턴합니다.
  • AuthenticationProvider는 전달 받은 UserDetails에 포함된 Credential(암호화 된 패스워드)과 인증되지 않은(unauthenticated) Authentication 객체에 포함된 password를 암호화 해서 둘을 비교합니다.
    • 두 패스워드를 비교해서 검증에 성공하면 인증된(authenticated) Authentication 객체를 JwtAuthenticationFilter에게 전달합니다.
    • 두 패스워드를 비교해서 검증에 실패하면 AuthenticationException을 throw합니다.
  • JwtAuthenticationFilter는 전달 받은 인증된(authenticated) Authentication을 이용해 Access Token과 Refresh Token을 생성해 response header에 추가합니다.

 

 

JWT를 이용한 클라이언트 자격 검증 프로세스

[그림 1-2] JWT를 이용한 클라이언트 자격 검증 프로세스

클라이언트 측에서 JWT를 성공적으로 전달 받았다면 이제 전달 받은 Access Token을 이용해서 백엔드 애플리케이션의 API 엔드포인트에 접근할 수 있습니다.

 

[그림 1-2]의 JWT를 이용한 클라이언트 자격 검증 프로세스는 다음과 같습니다.

  • 클라이언트는 백엔드 애플리케이션의 API 엔드포인트를 이용하기 위해 request를 전송할 때 마다 request header에 Access Token을 실어보냅니다.
  • JwtVerificationFilter는 전달 받은 Access Token이 유효한 JWT인지를 검증하기 위해 JwtTokenizer에게 JWT 검증을 요청합니다.
  • JwtTokenizer는 Access Token의 Signature를 검증합니다.
    • Access Token이 유효한 JWT이면 Claims를 리턴합니다.
    • Access Token이 유효하지 않으면 Exception을 throw합니다.
  • JwtVerificationFilter는 전달 받은 Claims를 이용해 username과 role 정보를 포함한 Authentication 객체를 SecurityContext 저장합니다.
    • 이제 인증된(authenticated) Authentication 객체가 SecurityContext에 저장되어 있으므로 DispatcherServlet에 도달하기 전의 마지막 Security Filter까지 문제 없이 통과할 수 있습니다.
  • DispatcherServlet부터는 Spring MVC의 기본적인 요청 처리 흐름에서 알 수 있듯이 Controller까지 요청이 전달됩니다.

클라이언트 쪽에서 전송한 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를 이해하는데 도움이 되셨길 바랍니다.

 

Spring Security의 권한 부여 처리 흐름

  • Spring Security Filter Chain에서 URL을 통해 사용자의 액세스를 제한하는 권한 부여 Filter는 바로 AuthorizationFilter 이다.
    • SecurityContextHolder로 부터 Authentication을 획득
  • SecurityContextHolder로 부터 획득한Authentication과 HttpServletRequest를 AuthorizationManager에게 전달
  • AuthorizationManager는 권한 부여 처리를 총괄하는 매니저 역할을 한다.
  • RequestMatcherDelegatingAuthorizationManagerAuthorizationManager를 구현하는 구현체 중 하나이다.
    • RequestMatcherDelegatingAuthorizationManagerRequestMatcher 평가식을 기반으로 해당 평가식에 매치되는 AuthorizationManager에게 권한 부여 처리를 위임하는 역할
    • RequestMatcherDelegatingAuthorizationManager가 직접 권한 부여 처리를 하는 것이 아니라 RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 위임
    • RequestMatcherDelegatingAuthorizationManager 내부에서 매치되는 AuthorizationManager 구현 클래스가 있다면 해당 AuthorizationManager 구현 클래스가 사용자의 권한을 체크
  • 적절한 권한이 아니라면 (5)와 같이 AccessDeniedException이 throw되고 ExceptionTranslationFilter가 AccessDeniedException을 처리

Spring Security 인증 컴포넌트의 인터랙션 과정

 

  • UsernamePasswordAuthenticationFilter 는 Username/Password 기반의 인증 요청을 처리한다.
    • UsernamePasswordAuthenticationToken 생성
      • 아직 인증되지 않은 Authentication 객체
  • AuthenticationManager는 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스.
  • ProviderManager는 AuthenticationManager를 구현한 구현 클래스
  • UserDetails는 사용자의 자격을 증명해주는 크리덴셜(Credentials) 과 권한 정보를 가지고 있는 객체.
  • UserDetailsService가 사용자의 Credential과 권한 정보를 조회해서 UserDetails 를 생성.
  • UserDetailsService가 AuthenticationProvider에게 UserDetails 를 전달.
  • AuthenticationProvider는 전달 받은 UserDetails에서 패스워드가 일치하는지 검증.
  • AuthenticationProvider가 패스워드 검증에 성공하면 인증에 성공한 사용자의 정보(Principal, Credential, GrantedAuthorities)를 포함한 Authentication을 생성.
  • 인증된 Authentication을 전달 받은 UsernamePasswordAuthenticationFilterSecurityContextHolder를 이용해 SecurityContext인증된 Authentication을 저장.
    SecurityContext는 다시 HttpSession에 저장되어 사용자의 인증 상태를 유지.

 

Spring Security의 인증 컴포넌트

1️⃣ UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter

  • Username/Password 방식의 인증 처리의 시작점

2️⃣ UsernamePasswordAuthenticationToken

  • 사용자의 인증 처리가 되기 전에는 인증되지 않은 Authentication.
    • Username과 Password 정보를 포함하고 있다.
  • 사용자의 인증 처리가 된 이후에는 인증된 Authentication
    • Principal, Authorities를 포함하고 있다.
    • Credentials 정보는 민감한 정보이므로 인증 이 후에는 제거된다.

3️⃣ AuthenticationManager

  • 인증 처리를 지시하는 매니저 역할을 한다.
  • 인증 매니저 계의 총 지배인

4️⃣ ProviderManager

  • AuthenticationManager의 구현 클래스
  • 인증 매니저 계의 지배인
  • 인증이 성공적으로 이루어진 후, Crendentials를 제거한다.
    • 인증 끝났으니까 민감한 정보는 가지고 있을 필요가 없음.

5️⃣ AuthenticationProvider

  • 인증된 사용자인지를 판단하는 역할
  • 인증 매니저의 지시를 받는 현장 담당자 역할

6️⃣ UserDetails

  • 크리덴셜 저장소에 저장된 크리덴셜 정보를 가지고 있는 객체
  • AuthenticationProvider는 UserDetailsService로부터 UserDetails를 전달 받는다.

7️⃣ UserDetailsService

  • UserDetails 객체를 생성해서 AuthenticationProvider에게 제공하는 역할을 한다.

8️⃣ SecurityContext와 SecurityContextHolder

  • 인증된 Authentication 정보를 저장하는 저장 객체
  • SecurityContext는 인증 성공 이 후, HTTP Session에 저장된다.
  • HTTP Session은 최종적으로 StandardManager의 상위 클래스인 ManagerBase에서 관리된다.
  • HTTP Session에 누가 저장 할까?
    • 최초 인증에 성공할 경우에는 UsernamePasswordAuthenticationFilter에서  HttpSessionSecurityContextRepository를 이용해 HTTP Session에 저장한다.
    • 인증 이 후에 재요청 시에는 SecurityContextPersistenceFilter가 저장한다.
      • 인증 이 후 재요청 시의 프로세스는 대략 아래와 같다.
        • HTTP 요청
        • --> SecurityContextPersistenceFilter가 HTTP Session에서 SecurityContext를 꺼낸 후, SecurityContextHolder에 저장
        • -> 이 후, 요청이 끝나는 시점에 SecurityContextPersistenceFilter의 finally 블럭에서
          • SecurityContextHolder에 저장된 SecurityContext를 지운 뒤,
          • --> HttpSessionSecurityContextRepository를 이용해 HTTP Session에 SecurityContext를 다시 저장한다.

SecurityContext의 구조

 

우리가 일반적으로 생각하는 대표적인 암호화의 대상은 크리덴셜(Credential)의 한 종류인 패스워드입니다. 패스워드는 암호화의 대상인 메시지를 조금 더 구체적으로 표현한 것이라고 볼 수 있을테구요.

이번 시간에는 암호화에 대한 아주 쉬운 이야기를 짧게 해보려고 합니다.

암호화란 무엇인지 암호화 유형에는 기본적으로 어떤것이 있는지 등을 아주 쉽게 이해할 수 있도록 짧게 정리해 보겠습니다.

 

암호화란?

  • 사용자의 민감한 정보를 보호하기 위한 기술

  • Crypto 또는 Cipher라는 비슷한 용어가 있음

  • Ciphertext: 암호화 된 텍스트

  • Cryptography
    • 암호학적 관점에서의 암호화 전체를 아우르는 말
  • Crypto Currency
    • 암호 화폐
  • Encryption / Decryption
    • 암호학 관점에서의 데이터 변환 과정
    • Encryption
      • 암호화
    • Decryption
      • 복호화
  • Encoding / Decoding
    • 암호학 관점은 아님
    • 데이터의 형식이나 패턴을 목적에 맞게 변환하는 과정
    • 예)
      • Encoding
        • encode(원본 데이터) → Base64 형식의 문자열로 인코딩
      • Decoding
        • decode(Base64 형식으로 인코딩된 문자열) → 원본 데이터


암호화 대상의 기준

웹 애플리케이션의 아이디/패스워드 로그인을 생각해보면 암호화의 대상 기준은 무엇이 되어야하는가를 잘 알 수 있습니다.

패스워드는 민감한(Sensitive) 정보이므로 타인에게 평문(Plain Text) 형태로 유출이 되면 안될테니까요.

따라서 애플리케이션 구현시 외부에 유출되면 문제가 발생할 수 있는 정보들은 암호화의 대상이 될 수 있으며, 아래에서 나열한 항목들은 암호화를 진지하게 고려해 보아야 하는 대상이라고 볼 수 있습니다.

  • 로그인 인증을 위한 패스워드
  • 구글, AWS 등의 외부 서비스를 이용하기 위한 계정 정보 또는 Secret 정보
  • JWT를 생성하고 검증하는데 필요한 Secret 정보
  • 데이터베이스 접속 정보
    • 데이터베이스 접속 정보에서 root 계정을 사용하는 분들이 많은데 root 계정이 아닌 별도의 사용자 계정을 만들어서 사용하길 권장합니다.
    • root는 말 그대로 최고 권한을 가진 계정이기 때문에 직접적으로 사용하지 않는 것이 좋습니다.

 

암/복호화 가능 여부에 따른 암호화 방식

  • 양방향 암호화
    • 암호화도 되고 복호화도 되는 암호화 방식
    • 암호화 된 메시지를 상대방이 볼 수 있어야 하는 암호화 방식이며, 복호화가 되어야 정상적인 메시지를 확인할 수 있습니다.

  • 단방향 암호화
    • 암호화는 되지만 복호화는 되지 않는 암호화 방식
    • 암호화된 메시지를 굳이 복호화 해서 확인할 필요가 없을 때 사용할 수 있는 암호화 방식입니다.
    • 단방향 암호화를 적용하는 대표적인 항목이 로그인 패스워드입니다. 로그인 패스워드의 경우 사용자가 입력한 패스워드를 암호화 한 데이터와 이미 암호화 되어 저장되어 있는 데이터가 일치한지 여부만 판단하면 되기 때문에 복호화가 필요없습니다.
    • 단방향 암호화는 일반적으로 해시 알고리즘을 사용하는데 엄밀하게 말하자면 해시 알고리즘 자체가 암호화라고 보기는 어렵지만 어쨌든 원본 메시지를 보호할 수 있다는 관점에서의 암호화라고 생각하면 좋을 것 같습니다.

 

 키 유형에 따른 암호화 방식

  • 대칭키 암호화 방식
    • 하나의 키를 서로 공유해서 해당 키로 암호화/복호화를 수행
    • 대칭키 암호화 방식은 하나의 키를 서로 공유해야 하기 때문에 공유된 키가 유출되면 심각한 문제가 발생할 수 있습니다.

  • 비대칭키 암호화 방식
    • 암호화를 수행하는 키와 복호화를 수행하는 키가 다릅니다.
    • Public Key와 Private Key
    • Public Key로 암호화 하는 것은 민감한 데이터 또는 메시지 보호를 위해서.
    • Private Key로 암호화 하는 것은 디지털 서명(Digital Signature)을 위해서.
      • 블록체인에서 이 방식을 사용합니다.

 

HTTPS의 동작 원리와 CA를 그림으로 표현한다면 한마디로 아래와 같습니다.

HTTPS의 동작 원리

HTTPS의 동작 원리

 

CA의 역할

CA의 역할

+ Recent posts

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