이번 글에서는 Spring Security 애플리케이션이 인증된 유저에게 권한을 부여하고 관리하는 방식에 대해 알아보겠습니다. 참고 자료는 다음과 같습니다.

GrantedAuthority

우선 Spring Security는 GrantedAuthority를 통해 Authentication이 가진 권한을 나타냅니다.

GrantedAuthority는 AuthenticationManager가 인증을 수행할 때 Authentication에 추가되며, AccessDecisionManager가 인가 결정을 내릴 때 읽어 사용합니다.

image.png

GrantedAuthority는 단 하나의 메서드를 가지고 있습니다. 보유한 권한을 문자열로 반환하는 메서드입니다.

한편, 권한의 이름을 네이밍할 때 접두사를 사용합니다. 기본적으로 ROLE_을 붙입니다. 예를 들어 관리자의 권한은 ROLE_ADMIN으로 나타낼 수 있습니다. 물론 접두사를 변경할 수도 있습니다.

계층적인 역할

일상에서 역할은 다른 역할도 포함하는 경우가 많습니다. 예를 들어, 쇼핑몰 애플리케이션에서 우수 판매자는 우수 판매자이면서 판매자이고 유저입니다. 역시나 Spring Security에서도 이러한 계층적인 역할을 표현할 수 있습니다.

@Bean
static RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.withDefaultRolePrefix()
        .role("TOP_SELLER").implies("SELLER")
        .role("SELLER").implies("USER")
        .role("USER").implies("GUEST")
        .build();
}

// 만약 @PreAuthorize와 같은 메서드 보안 애너테이션을 사용하는 경우 아래의 코드를 추가한다.
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
	DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
	expressionHandler.setRoleHierarchy(roleHierarchy);
	return expressionHandler;
}

AuthorizationManager

보안 요소에 요청이 접근하는 경우 Authorizationmanager가 이에 대한 인가 결정을 내립니다. 이때, 요청 그 자체 혹은 메서드, 메시지를 기반으로 결정을 내릴 수 있으며 아래 두 메서드로 동작합니다.

image.png

우선 check()의 경우 인가 결정에 필요한 모든 정보를 매개 변수로 전달받아 AuthorizationDecision을 반환합니다. 예를 들어 특정 고객 정보를 사용하는 메서드에 접근한다고 가정했을 때, 보호할 객체 T object는 메서드 호출인 MethodInvocation이며 매개변수 고객 Customer과 Authentication을 비교하여 접근할 수 있는지 판단할 수 있습니다.

AuthorizationDecision은 내부에 인가 결정의 결과를 가지고 있습니다.

image.png

반환하는 AuthorizationDecision는 총 3가지로 나뉩니다.

  • 접근할 수 있는 경우: true 값을 가지는 AuthorizationDecision을 반환
  • 접근할 수 없는 경우: false 값을 가지는 AuthorizationDecision를 반환
  • 판단할 수 없는 경우: null을 반환하여 결정을 미룸

verfiy()의 경우 내부적으로 check()를 호출하며 반환 값인 AuthorizationDecision에 따라 AccessDeniedException를 던집니다.

image.png

인가 요청 흐름

지금까지 권한 모델 GrantedAuthority과 인가를 담당하는 컴포넌트 AuthorizationManager에 대해 알아보았습니다. 이를 이용하여 Spring Security의 전체에서 어떻게 인가 요청이 처리되는지 알아보겠습니다.

image.png

  1. Spring Security에 클라이언트의 요청이 FilterChain을 거쳐 AuthorizationFilter에 도착합니다. AuthorizationFilter는 SecurityContextHolder에서 Authentication을 얻는Supplier을 생성합니다.
  2. AuthorizationFilter는 Supplier와 HttpServletRequest를 AuthorizationManager에 전달합니다. AuthorizationManager는 authorizeHttpRequest에 있는 패턴으로 요청을 구분해 인가 처리를 수행합니다.
  3. 인가가 거부된 경우 AuthorizationDeniedEvent를 발행하고 AccessDeniedException을 던집니다. 이는 나중에 ExceptionTranslationFilter에 의해 처리됩니다.
  4. 인가가 허락된 경우, AuthorizationGrantedEvent를 발행하고 AuthorizationFilter의 남은 코드를 실행하여 FilterChain을 계속 진행합니다.

Authentication을 Supplier로 전달하는 이유

위 흐름에서 AuthorizationFilter는 Authentication이 아닌 Supplier에 감싸 이를 전달합니다. 이는 요청이 항상 허용되거나 항상 거부되는 경우 굳이 Authetication 객체가 필요하지 않기 때문입니다. 따라서 Authentication을 필요할때만 조회함으로써 인가 처리를 빠르게 할 수 있습니다.

인가 필터의 위치

일반적으로 AuthorizationFilter는 SecurityFilterChain의 가장 마지막에 위치합니다. 따라서 인증 필터와 같은 모든 필터는 인가에 대한 권한이 필요하지 않습니다.

또한, Spring Security는 Servlet Filter를 기반으로 동작하므로 Spring MVC 이전에 존재합니다. 다시 말해, 사용자의 요청은 Spring Security의 모든 필터를 거친 후 DispatcherServlet으로 전달됩니다. 따라서 Spring MVC의 엔드포인트에 접근하려면 Spring Security에서 인가 설정을 해야합니다.

모든 Dispatch는 인가가 필요하다

한편, AuthorizationFilter는 모든 요청에 대해 동작하는 건 아닙니다. 그러나 모든 디스패처에 대해 동작하기 때문에 유의해야합니다. 즉, REQUEST 뿐만 아니라 FORWARD, ERROR, INCLUDE 모두 인가가 필요합니다.

예를 들어, 아래와 같이 Thymeleaf 템플릿을 이용해서 뷰 리졸버에게 FORWARD하는 코드가 있다고 해봅시다.

@Controller
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() {
        return "endpoint";
    }
}

이때 Spring Security의 인가는 두 번 동작합니다. 첫 번째는 /endpoint에 접근할 때, 두 번째는 뷰 리졸버에게 FORWARD 할 때 발생합니다. 이는 ERROR의 경우에 동일하게 동작합니다.

일반적으로 FORWARD와 ERROR와 같은 디스패처를 처리할 때 인가 작업은 필요하지 않으므로 이를 인가 처리에서 제외하는 것이 바람직합니다.

http
    .authorizeHttpRequests((authorize) -> authorize
        .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
        .requestMatchers("/endpoint").permitAll()
        .anyRequest().denyAll()
    )

마무리하며

이번 글에서는 Spring Security의 인가에 대해 알아보았습니다. 이번 글을 요약하면 다음과 같습니다.

  • GrantedAuthority는 Authentication이 가진 권한을 나타낸다.
    • AuthenticationManager가 Authentication에 추가한다.
    • AccessDesionManager가 Authentication로부터 읽어 사용한다.
  • Spring Security는 RoleHierarchy를 통해 계층적 역할 구조를 지원한다.
  • AuthorizationManager는 보안 요소에 대한 인가 처리를 수행한다.
  • AuthenticationFilter는 FilterChain의 마지막에 위치한다.
  • 모든 요청에 대해 인가가 필요한 것은 아니지만, 모든 디스패처는 인가가 필요하다.

댓글남기기