Authorization Architecture

김상욱·2024년 12월 9일

Spring Security의 권한(Authorities) 개념과 구현 방식

1. GrantedAuthority란?

Spring Security에서 GrantedAuthority는 인증된 사용자가 가진 개별적인 권한을 나타냅니다.

  • 사용자는 여러 개의 권한을 가질 수 있습니다.
  • 이 권한들은 사용자의 역할(Role) 또는 특정 작업에 대한 권한(Action Permission)을 나타낼 수 있습니다.

주요 역할:

  • Spring Security에서 권한을 기반으로 접근 제어(Authorization)를 처리합니다.

2. GrantedAuthority의 구조

Spring Security의 GrantedAuthority는 다음과 같은 단순한 인터페이스입니다:

public interface GrantedAuthority {
    String getAuthority();
}
  • getAuthority() 메서드: 권한을 문자열(String)로 반환합니다.
    • 예: ROLE_USER, ROLE_ADMIN, 또는 사용자 정의 권한 문자열.
  • 이 문자열은 Spring Security가 특정 권한을 검사할 때 활용됩니다.

3. 권한 객체의 흐름

Spring Security의 인증 및 권한 부여 과정에서 GrantedAuthority는 다음과 같은 흐름으로 사용됩니다:

  1. AuthenticationManager에서 권한 생성

    • 사용자가 인증되면 AuthenticationManager는 사용자의 권한 정보를 GrantedAuthority로 변환하여 Authentication 객체에 저장합니다.
  2. AccessDecisionManager에서 권한 확인

    • Spring Security가 접근을 제어할 때, 저장된 GrantedAuthority 목록을 검사하여 사용자가 특정 요청에 대해 권한이 있는지 판단합니다.

4. 간단한 구현체: SimpleGrantedAuthority

Spring Security에서 기본적으로 제공하는 GrantedAuthority의 구현체는 SimpleGrantedAuthority입니다.

  • 이 클래스는 단순한 문자열을 GrantedAuthority로 변환합니다.
  • 예:
    new SimpleGrantedAuthority("ROLE_USER");

5. 복잡한 권한: Complex GrantedAuthority

일부 상황에서는 권한이 문자열로 간단히 표현될 수 없을 수도 있습니다.
예:

  • 특정 고객 계정에서만 권한을 허용하는 경우.
  • 권한이 여러 조건(예: 작업 유형 및 임계값)에 따라 달라지는 경우.

이런 경우, getAuthority() 메서드는 null을 반환해야 합니다.

  • Spring Security는 이 값을 보고, 해당 권한을 처리할 수 있는 특정 구현이 필요하다는 것을 인식합니다.

6. 역할(Role) 기반 권한의 접두사

Spring Security는 기본적으로 역할(Role) 기반 권한에 접두사 ROLE_을 사용합니다.

  • 예를 들어, "USER"라는 역할이 필요하다면, Spring Security는 GrantedAuthority의 값으로 ROLE_USER를 기대합니다.

이를 변경하고 싶다면 GrantedAuthorityDefaults를 사용하여 접두사를 커스터마이징할 수 있습니다.

접두사 커스터마이징 코드

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("MYPREFIX_");
}
  • 위 코드는 ROLE_ 대신 MYPREFIX_를 접두사로 사용합니다.
  • 결과적으로, MYPREFIX_ADMIN 또는 MYPREFIX_USER와 같은 권한 이름을 사용할 수 있습니다.

7. 권한 부여(Authorization)의 처리 방식

Spring Security에서 권한 부여는 크게 3가지 수준에서 처리됩니다:

1) HTTP 요청 수준 (URI 권한 부여)

  • 특정 URL 또는 요청 메서드에 대해 권한을 제한합니다.
  • 예:
    http
        .authorizeRequests()
        .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
        .antMatchers("/user/**").hasAuthority("ROLE_USER")
        .anyRequest().authenticated();

2) 메서드 수준 (Method Security)

  • Spring Security는 어노테이션 기반으로 메서드 접근을 제어할 수 있습니다.
  • 예:
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public void adminMethod() {
        // 관리자만 접근 가능
    }

3) 도메인 객체 수준 (Domain Object Security)

  • 객체별로 접근 권한을 세밀하게 제어할 수 있습니다.
  • Access Control List(ACL) 기반으로 구현됩니다.
  • 예: 특정 사용자가 특정 리소스에만 접근 가능하도록 설정.

8. 권한 이벤트 리스닝

Spring Security는 각 권한 검사를 이벤트로 발행하며, 이를 통해 특정 권한 부여 로직을 모니터링하거나 커스터마이징할 수 있습니다.

  • 권한 부여 이벤트를 청취하고 필요한 동작을 수행할 수 있습니다.

정리

  1. GrantedAuthority는 사용자의 권한을 나타내는 기본 단위입니다.
  2. Spring Security는 기본적으로 SimpleGrantedAuthority를 사용하지만, 복잡한 구현도 지원합니다.
  3. 기본 접두사 ROLE_GrantedAuthorityDefaults를 사용해 변경할 수 있습니다.
  4. 권한 부여는 HTTP 요청, 메서드, 도메인 객체 수준에서 다룰 수 있으며, 이벤트 기반 커스터마이징도 가능합니다.

이렇게 설계된 Spring Security의 권한 구조는 유연하면서도 강력한 권한 부여 메커니즘을 제공합니다.


호출 처리(Invocation Handling)

Spring Security는 메서드 호출이나 웹 요청과 같은 보안 객체에 대한 접근을 제어하는 인터셉터를 제공합니다.

  • 사전 호출 결정(Pre-invocation decision): 호출을 계속 진행할 수 있는지 여부를 AuthorizationManager 인스턴스가 판단합니다.
  • 사후 호출 결정(Post-invocation decision): 특정 값을 반환할 수 있는지 여부를 AuthorizationManager 인스턴스가 판단합니다.

Spring Security의 AuthorizationManager 상세 설명

1. AuthorizationManager란?

Spring Security의 AuthorizationManager최종적인 권한 부여 결정을 내리는 컴포넌트입니다.
기존의 AccessDecisionManagerAccessDecisionVoter와 동일한 역할을 수행하지만, 더 유연하고 통합된 설계를 제공합니다.

2. 주요 특징

  1. 새로운 권한 부여 구조

    • 기존의 AccessDecisionManagerAccessDecisionVoter를 대체하며, 더 간단한 API를 제공합니다.
    • 권한 부여 로직을 한 곳에서 통합적으로 관리할 수 있습니다.
  2. 유연성

    • 요청(HTTP), 메서드 호출, 메시지 처리 등 다양한 보안 컨텍스트에서 일관된 권한 부여 방식을 제공합니다.
  3. 권한 부여 방식

    • 권한 부여는 요청 또는 메서드의 매개변수를 포함한 보호 객체(secureObject)를 기반으로 수행됩니다.
    • 사용자가 특정 리소스에 접근할 수 있는지에 대한 세밀한 제어를 지원합니다.

3. AuthorizationManager의 메서드 설명

1) check 메서드

AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
  • 역할

    • 권한 부여를 결정하는 핵심 메서드로, 사용자가 특정 리소스에 접근할 권한이 있는지를 판단합니다.
  • 매개변수

    1. Supplier<Authentication>

      • 인증된 사용자의 정보를 제공합니다.
      • 이 객체는 Spring Security의 인증 과정을 통해 생성됩니다.
        예: Authentication 객체에서 현재 사용자의 권한, 역할, 사용자 이름 등을 확인할 수 있습니다.
    2. Object secureObject

      • 보호 대상 객체입니다.
      • 호출된 메서드, 요청된 URL, 또는 메시지 같은 다양한 리소스를 포함할 수 있습니다.
        예:
        • HTTP 요청 → URL, HTTP 메서드 (GET, POST)
        • 메서드 호출 → MethodInvocation 객체 (메서드 이름 및 매개변수 포함)
  • 리턴값

    • AuthorizationDecision 객체를 반환하며, 권한 부여 결과를 나타냅니다:
      1. 긍정적(Positive):
        접근 허용 시 AuthorizationDecision 객체가 허용(true) 값을 포함하여 반환됩니다.
      2. 부정적(Negative):
        접근 거부 시 AuthorizationDecision 객체가 거부(false) 값을 포함하여 반환됩니다.
      3. 기권(Abstain):
        권한 부여 판단을 하지 않을 경우 null 반환.

2) verify 메서드

default void verify(Supplier<Authentication> authentication, Object secureObject)
        throws AccessDeniedException {
    // ...
}
  • 역할

    • check 메서드를 호출하여 권한을 검사한 후, 결과가 거부(Negative)일 경우 AccessDeniedException을 발생시킵니다.
    • 권한이 없는 사용자가 리소스에 접근하려는 경우, 예외를 던져 오류 처리를 쉽게 할 수 있습니다.
  • 동작 순서

    1. 내부적으로 check 메서드를 호출하여 권한 부여 결과를 확인합니다.
    2. 결과가 부정적(Negative)일 경우, AccessDeniedException을 발생시켜 애플리케이션에서 적절한 조치를 취할 수 있도록 합니다.

4. 동작 예제

예: 메서드 호출을 검사하는 AuthorizationManager

사용자가 특정 고객(Customer) 데이터를 조회할 권한이 있는지 확인하는 로직입니다.

  1. 보호 대상: secureObject

    • 메서드 호출(MethodInvocation) 객체를 통해 메서드 이름 및 매개변수를 확인합니다.
    • 예: getCustomerDetails(Long customerId)
  2. 권한 부여 로직:

    • 사용자가 인증되었는지 확인.
    • 사용자가 요청한 고객 ID와 일치하는 권한이 있는지 확인.
  3. 코드 예제:

public class CustomerAuthorizationManager implements AuthorizationManager<MethodInvocation> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // 인증 정보 가져오기
        Authentication auth = authentication.get();
        String principal = auth.getName(); // 현재 사용자 이름 가져오기

        // 호출된 메서드의 인수 확인
        Object[] args = invocation.getArguments();
        Long customerId = (Long) args[0]; // 고객 ID 추출

        // 현재 사용자가 고객 데이터를 조회할 권한이 있는지 검사
        boolean hasPermission = checkPermission(principal, customerId);
        return new AuthorizationDecision(hasPermission); // 권한 결과 반환
    }

    private boolean checkPermission(String principal, Long customerId) {
        // 실제 권한 검사 로직
        return "admin".equals(principal) || isCustomerOwner(principal, customerId);
    }

    private boolean isCustomerOwner(String principal, Long customerId) {
        // 사용자가 고객 데이터를 소유하고 있는지 확인하는 로직
        return true; // 예시를 위한 간단한 로직
    }
}

5. 기존 방식과의 차이점

  1. 단순화된 설계

    • AccessDecisionManagerAccessDecisionVoter에서는 복잡한 투표 기반 권한 부여 로직이 필요했지만, AuthorizationManager는 이를 단일 메서드로 처리.
  2. 유연성 향상

    • secureObject를 통해 요청 및 메서드 호출의 모든 세부 정보를 직접 확인 가능.
  3. 권한 부여 및 예외 처리 통합

    • verify 메서드를 통해 권한 검사의 결과를 기반으로 간단하게 예외 처리 가능.

6. 실무에서의 활용

  • HTTP 요청: URL 기반 권한 부여.

    http.authorizeRequests()
        .antMatchers("/admin/**").access(new AuthorizationManager<>(...))
        .anyRequest().authenticated();
  • 메서드 호출:
    메서드 수준의 권한 검사.

    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public void adminMethod() {
        // 관리자만 접근 가능
    }

7. 요약

  • AuthorizationManager는 Spring Security의 새로운 권한 부여 표준입니다.
  • HTTP 요청, 메서드 호출, 메시지 처리 등에서 사용 가능.
  • checkverify를 통해 권한 검사를 간단하고 유연하게 처리.
  • 기존 구조보다 더 간결하며, 다양한 보호 대상에 대해 세밀한 제어를 제공합니다.

Spring Security의 Delegate 기반 AuthorizationManager 구현

1. Delegate 기반 접근이란?

Delegate 기반 접근 방식에서는 여러 AuthorizationManager 인스턴스를 조합하여 권한 부여를 처리합니다.

  • AuthorizationManager는 특정 조건 또는 맥락에 따라 독립적으로 작동합니다.
  • Delegating AuthorizationManager는 이를 조정하고, 요청이나 호출된 메서드에 따라 적절한 AuthorizationManager를 선택하여 사용합니다.

2. 주요 Delegate 기반 구현

1) RequestMatcherDelegatingAuthorizationManager

이 구현체는 HTTP 요청 권한 부여를 위해 설계되었습니다.

  • 역할: HTTP 요청을 특정 조건(RequestMatcher)과 비교한 후, 가장 적합한 AuthorizationManager를 사용해 권한 부여를 처리합니다.

  • 작동 방식:

    1. 요청이 들어오면, 각 RequestMatcher가 요청과 일치하는지 검사합니다.
    2. 요청과 일치하는 RequestMatcher가 있으면, 해당 조건에 연결된 AuthorizationManager가 호출됩니다.
    3. 일치하지 않는 경우, 기본 처리 로직(Default Policy)이 수행됩니다.
  • 사용 예제:

    var authorizationManager = RequestMatcherDelegatingAuthorizationManager.builder()
        .add(RequestMatchers.antMatcher("/admin/**"), new AuthorizationManager<>() {
            // 관리자 영역에 대한 권한 부여 로직
            @Override
            public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
                return new AuthorizationDecision(authentication.get().getAuthorities()
                        .contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
            }
        })
        .add(RequestMatchers.antMatcher("/user/**"), new AuthorizationManager<>() {
            // 사용자 영역에 대한 권한 부여 로직
            @Override
            public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
                return new AuthorizationDecision(authentication.get().isAuthenticated());
            }
        })
        .build();

2) 메서드 보안: AuthorizationManagerBeforeMethodInterceptorAuthorizationManagerAfterMethodInterceptor

Spring Security는 메서드 호출 시 권한 부여를 제어하기 위해 사전/사후 인터셉터를 제공합니다.

  1. AuthorizationManagerBeforeMethodInterceptor

    • 역할: 메서드 호출 전에 권한을 확인합니다.
    • 사용 예: 호출된 메서드와 매개변수를 기반으로 권한 검사를 수행합니다.
    • 메서드 호출 전에 권한이 없는 경우, 호출을 차단하고 예외를 발생시킵니다.
    AuthorizationManagerBeforeMethodInterceptor beforeInterceptor =
        AuthorizationManagerBeforeMethodInterceptor.builder()
            .expression("hasRole('ROLE_ADMIN')") // 관리자 권한 확인
            .build();
  2. AuthorizationManagerAfterMethodInterceptor

    • 역할: 메서드 호출 후에 반환값에 대한 권한 검사를 수행합니다.
    • 사용 예: 반환값을 기준으로 권한 부여를 검사합니다.
    • 메서드 실행 후 반환값이 권한 검사 조건을 충족하지 않을 경우, 예외를 발생시킬 수 있습니다.
    AuthorizationManagerAfterMethodInterceptor afterInterceptor =
        AuthorizationManagerAfterMethodInterceptor.builder()
            .expression("returnObject.owner == authentication.name") // 반환값 소유권 검사
            .build();

3. 권한 부여 로직의 구성(Composition)

Delegate 기반 AuthorizationManager를 사용하면 여러 권한 부여 로직을 조합하여 유연한 보안 정책을 구현할 수 있습니다.

  • AuthorizationManager특정 맥락이나 조건에 따라 독립적으로 작동합니다.
  • 최종적으로 Delegating AuthorizationManager가 이들을 호출하여 권한 부여 결정을 내립니다.

4. 예제: 권한 부여 로직 조합

권한 부여 시나리오

  • /admin/ 경로: 관리자만 접근 가능
  • /user/ 경로: 인증된 사용자만 접근 가능
  • 기타 요청: 접근 불가

구현 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    var authorizationManager = RequestMatcherDelegatingAuthorizationManager.builder()
        // 관리자 요청 처리
        .add(RequestMatchers.antMatcher("/admin/**"), new AuthorizationManager<>() {
            @Override
            public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
                return new AuthorizationDecision(authentication.get().getAuthorities()
                        .contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
            }
        })
        // 사용자 요청 처리
        .add(RequestMatchers.antMatcher("/user/**"), new AuthorizationManager<>() {
            @Override
            public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
                return new AuthorizationDecision(authentication.get().isAuthenticated());
            }
        })
        // 기본 정책 (접근 불가)
        .add(RequestMatchers.anyRequest(), new AuthorizationManager<>() {
            @Override
            public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
                return new AuthorizationDecision(false); // 접근 불가
            }
        })
        .build();

    http.authorizeHttpRequests(auth -> auth.requestMatcherDelegatingAuthorizationManager(authorizationManager));
    return http.build();
}

5. 권한 부여 결정의 계층적 구조

  • Spring Security는 권한 부여 로직을 계층적으로 설계할 수 있습니다.
  • 계층 구조 예:
    1. 요청 기반 권한 부여 (RequestMatcherDelegatingAuthorizationManager)
    2. 메서드 기반 권한 부여 (AuthorizationManagerBeforeMethodInterceptor)
    3. 도메인 객체 수준 권한 부여 (ACL 등)

구조 예시

요청 → RequestMatcherDelegatingAuthorizationManager → 적절한 AuthorizationManager 호출
메서드 → AuthorizationManagerBeforeMethodInterceptor → 권한 확인
결과 → AuthorizationManagerAfterMethodInterceptor → 반환값 확인

6. 요약

  1. Delegate 기반의 장점

    • 유연한 구성: 다양한 권한 부여 로직을 조합 가능.
    • 재사용성: 각 AuthorizationManager는 독립적이고 재사용 가능.
  2. 주요 Delegate 구현체

    • RequestMatcherDelegatingAuthorizationManager: HTTP 요청 권한 부여 처리.
    • AuthorizationManagerBeforeMethodInterceptor / AuthorizationManagerAfterMethodInterceptor: 메서드 호출 전/후 권한 부여 처리.
  3. 적용 사례

    • HTTP 요청, 메서드 호출, 반환값 확인 등 다양한 맥락에서 권한 부여를 유연하게 구성 가능.
    • 계층적 보안 정책을 통해 복잡한 애플리케이션 보안 요구사항을 충족.

Spring Security의 주요 AuthorizationManager 구현체와 사용자 정의 방법

1. AuthorityAuthorizationManager

가장 일반적으로 사용되는 권한 관리자이며, 사용자의 권한(Authority)을 기반으로 권한 부여를 결정합니다.

작동 원리

  • 구성:
    특정 권한(Authority) 목록을 설정합니다.
    이 목록은 현재 사용자(Authentication)의 권한 정보와 비교됩니다.
  • 권한 부여 흐름:
    1. 사용자의 Authentication 객체를 확인합니다.
    2. 사용자가 가진 권한 목록(GrantedAuthority)에 구성된 권한 중 하나라도 존재하면 긍정적(Positive) 결정을 반환합니다.
    3. 구성된 권한이 사용자에게 없으면 부정적(Negative) 결정을 반환합니다.

예제

// ROLE_ADMIN 또는 ROLE_USER 권한이 있어야 접근 가능
AuthorityAuthorizationManager<Object> authorityManager = 
    AuthorityAuthorizationManager.hasAnyAuthority("ROLE_ADMIN", "ROLE_USER");

AuthorizationDecision decision = authorityManager.check(() -> authentication, secureObject);

if (decision.isGranted()) {
    System.out.println("Access Granted");
} else {
    System.out.println("Access Denied");
}

적용 예

  • 관리자 영역 접근:
    관리자 권한이 있는 사용자만 접근할 수 있도록 설정.
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .access(AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"));

2. AuthenticatedAuthorizationManager

사용자가 인증 상태인지, 그리고 인증 방식에 따라 권한을 부여합니다.

작동 원리

  • 인증 상태를 구분하여 권한을 부여합니다:
    1. 익명 사용자: 인증되지 않은 상태.
    2. Remember-Me 인증 사용자: 제한된 인증 상태로, 일부 기능만 사용 가능.
    3. 완전 인증 사용자: 사용자 이름과 비밀번호 또는 강력한 인증 방법으로 인증된 상태.

활용 시나리오

  1. Remember-Me 인증 사용자 처리
    제한된 리소스 접근을 허용하고, 전체 접근을 위해 다시 로그인하도록 요구.
  2. 익명 사용자 차단
    인증되지 않은 사용자가 리소스에 접근하지 못하도록 설정.

예제

AuthenticatedAuthorizationManager<Object> authenticatedManager = 
    AuthenticatedAuthorizationManager.authenticated();

AuthorizationDecision decision = authenticatedManager.check(() -> authentication, secureObject);

if (decision.isGranted()) {
    System.out.println("Authenticated User Access Granted");
} else {
    System.out.println("Access Denied");
}

적용 예

  • Remember-Me 인증 처리:
    http.authorizeRequests()
        .antMatchers("/limited/**")
        .access(AuthenticatedAuthorizationManager.rememberMe());

3. AuthorizationManagers (정적 팩토리 메서드)

개요

Spring Security는 여러 AuthorizationManager를 조합하여 더 복잡한 권한 부여 로직을 작성할 수 있도록 정적 팩토리 메서드를 제공합니다.

활용 방법

  • 여러 AuthorizationManagerAND 또는 OR 조건으로 조합.
  • 복합적인 권한 검사를 쉽게 구현.

예제

// ROLE_ADMIN 권한을 가진 인증된 사용자만 접근 가능
AuthorizationManager<Object> composedManager = 
    AuthorizationManagers.allOf(
        AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"),
        AuthenticatedAuthorizationManager.authenticated()
    );

AuthorizationDecision decision = composedManager.check(() -> authentication, secureObject);

if (decision.isGranted()) {
    System.out.println("Access Granted");
} else {
    System.out.println("Access Denied");
}

조합 방식

  1. allOf: 모든 조건이 만족해야 함 (AND).
  2. anyOf: 하나의 조건만 만족해도 됨 (OR).

4. Custom Authorization Manager

Spring Security는 사용자가 직접 커스텀 AuthorizationManager를 구현할 수 있는 강력한 유연성을 제공합니다.

적용 가능성

  1. 애플리케이션 비즈니스 로직
    특정 사용자가 특정 리소스에 접근 가능한지 검사.
  2. 보안 관리 로직
    계정 상태, 접근 제한 정책 등을 실시간으로 확인.
  3. 외부 서비스와 연동
    Open Policy Agent(OPA) 또는 사용자 정의 권한 데이터베이스를 통해 권한 검사를 수행.

예제

  • 특정 사용자의 계정 상태에 따라 접근 권한을 제한하는 커스텀 AuthorizationManager.
public class AccountStatusAuthorizationManager implements AuthorizationManager<Object> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject) {
        Authentication auth = authentication.get();
        String username = auth.getName();

        // 계정 상태 확인 (예: 정지된 계정)
        if (isAccountSuspended(username)) {
            return new AuthorizationDecision(false); // 접근 거부
        }
        return new AuthorizationDecision(true); // 접근 허용
    }

    private boolean isAccountSuspended(String username) {
        // 외부 데이터베이스나 API를 통해 계정 상태 확인
        return false; // 예시로 항상 정상 계정으로 처리
    }
}

적용 예

  • 계정 상태 기반으로 접근 제한 설정:
    http.authorizeRequests()
        .antMatchers("/secure/**")
        .access(new AccountStatusAuthorizationManager());

5. 기존 AccessDecisionVoter와의 차이점

Spring Security 블로그에서 언급된 계정 상태 검사(AccessDecisionVoter)를 AuthorizationManager로 변환하는 방법은 다음과 같습니다:

AccessDecisionVoter 방식

이전 방식에서는 여러 AccessDecisionVoter를 조합하여 투표 기반으로 권한을 결정했습니다.

  • 단점:
    복잡한 구조로 인해 로직이 분산되고, 유지보수성이 낮았습니다.

AuthorizationManager 방식

  • 장점:
    단일 인터페이스로 권한 부여 로직을 간단히 작성할 수 있습니다.
    실시간으로 계정 상태를 확인하여 접근을 차단할 수 있습니다.

6. 요약

AuthorityAuthorizationManager

  • 특정 권한(Authority)을 기반으로 권한 부여.
  • 가장 일반적으로 사용되는 AuthorizationManager.

AuthenticatedAuthorizationManager

  • 인증 상태(익명, Remember-Me, 완전 인증)를 기반으로 권한 부여.
  • 제한된 접근과 전체 접근을 구분하는 데 사용.

AuthorizationManagers

  • 여러 권한 부여 로직을 조합하여 복합적인 권한 검사 구현.
  • allOf(AND), anyOf(OR)를 통해 조건 결합.

Custom Authorization Manager

  • 애플리케이션에 특화된 권한 부여 로직을 작성.
  • 외부 서비스와 연동하여 권한 검사 가능.

Spring Security의 AuthorizationManager는 기존 구조를 대체하면서도 더 간단하고 유연한 방식으로 권한 부여 로직을 처리할 수 있게 합니다.


AccessDecisionManager와 AccessDecisionVoter를 AuthorizationManager로 변환하기

이전 구조: AccessDecisionManager와 AccessDecisionVoter

Spring Security는 AuthorizationManager가 도입되기 이전에 AccessDecisionManagerAccessDecisionVoter를 사용하여 권한 부여를 처리했습니다.

적용 시나리오

  • 기존 애플리케이션을 업그레이드하거나, 과거 구조와 호환성을 유지해야 할 때, 기존 AccessDecisionManager 또는 AccessDecisionVoter를 호출하는 AuthorizationManager를 작성할 수 있습니다.

1. AccessDecisionManager를 호출하는 AuthorizationManager 구현

코드 예제

@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager<Object> {
    private final AccessDecisionManager accessDecisionManager;
    private final SecurityMetadataSource securityMetadataSource;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
        try {
            // 보호 객체의 보안 메타데이터 가져오기
            Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
            // AccessDecisionManager를 호출하여 권한 결정
            this.accessDecisionManager.decide(authentication.get(), object, attributes);
            return new AuthorizationDecision(true); // 접근 허용
        } catch (AccessDeniedException ex) {
            return new AuthorizationDecision(false); // 접근 거부
        }
    }

    @Override
    public void verify(Supplier<Authentication> authentication, Object object) {
        Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
        // AccessDecisionManager를 호출하여 검증
        this.accessDecisionManager.decide(authentication.get(), object, attributes);
    }
}

코드 설명

  1. check 메서드

    • SecurityMetadataSource를 통해 보호 객체(object)의 보안 메타데이터를 가져옵니다.
    • AccessDecisionManagerdecide 메서드를 호출하여 권한을 검사합니다.
    • 결과에 따라 긍정적 또는 부정적 AuthorizationDecision을 반환합니다.
  2. verify 메서드

    • check와 동일한 방식으로 동작하지만, 권한이 없을 경우 AccessDeniedException을 던져 예외 처리를 제공합니다.

2. AccessDecisionVoter를 호출하는 AuthorizationManager 구현

코드 예제

@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager<Object> {
    private final AccessDecisionVoter<Object> accessDecisionVoter;
    private final SecurityMetadataSource securityMetadataSource;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
        // 보호 객체의 보안 메타데이터 가져오기
        Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
        // AccessDecisionVoter 호출하여 투표 결과 결정
        int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
        switch (decision) {
        case AccessDecisionVoter.ACCESS_GRANTED:
            return new AuthorizationDecision(true); // 접근 허용
        case AccessDecisionVoter.ACCESS_DENIED:
            return new AuthorizationDecision(false); // 접근 거부
        }
        return null; // 기권(Abstain)
    }
}

코드 설명

  1. check 메서드
    • SecurityMetadataSource를 통해 보호 객체의 메타데이터를 가져옵니다.
    • AccessDecisionVotervote 메서드를 호출하여 투표 결과를 확인합니다.
    • 투표 결과가 ACCESS_GRANTED이면 접근을 허용하고, ACCESS_DENIED이면 접근을 거부합니다.
    • 투표 결과가 ABSTAIN인 경우, null을 반환하여 권한 결정을 하지 않습니다.

3. SecurityFilterChain에 연결하기

AccessDecisionManager 또는 AccessDecisionVoter를 사용하는 방식

위에서 작성한 AuthorizationManager를 Spring Security의 SecurityFilterChain에 연결하여 활용할 수 있습니다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
    AccessDecisionManagerAuthorizationManagerAdapter adapter) throws Exception {

    http.authorizeRequests()
        .antMatchers("/secure/**")
        .access(adapter) // Adapter를 권한 부여 로직으로 사용
        .anyRequest()
        .authenticated();

    return http.build();
}

4. 기존 방식과 AuthorizationManager의 차이

특징AccessDecisionManager/VoterAuthorizationManager
구조복잡한 투표 기반 설계. 다수의 Voter 조합 필요.단순한 인터페이스로 권한 검사 로직 작성.
유연성제한적. 특정 역할에 집중.커스터마이징 및 다양한 접근 방식 지원.
사용성새로운 애플리케이션에는 부적합.최신 권장 방식. 유지보수와 확장이 용이.
이전 방식과의 호환성기존 코드 재사용 가능.어댑터를 통해 이전 방식과 호환 가능.

5. 정리

  • AccessDecisionManager 또는 AccessDecisionVoterAuthorizationManager로 감쌀 수 있는 어댑터를 작성하면, 기존 애플리케이션에서 최신 권한 관리 방식을 도입할 수 있습니다.
  • SecurityFilterChain에 연결하여 권한 부여를 처리하며, 기존 구조와 새로운 구조를 유연하게 통합할 수 있습니다.
  • Spring Security의 최신 권장 방식은 AuthorizationManager를 사용하는 것이며, 이전 구조와의 호환성이 필요한 경우 위와 같은 어댑터 패턴을 활용할 수 있습니다.

Spring Security의 역할 계층 (Hierarchical Roles) 상세 설명

1. 역할 계층이란?

역할 계층(Hierarchical Roles)상위 역할이 하위 역할의 권한을 자동으로 포함하도록 정의하는 메커니즘입니다.
이를 통해 애플리케이션의 역할 관리와 접근 제어를 단순화할 수 있습니다.

예시

  • 애플리케이션에 ROLE_ADMINROLE_USER가 존재한다고 가정:
    • ROLE_ADMINROLE_USER의 모든 작업을 수행할 수 있어야 합니다.
    • 하지만 관리자를 위해 별도로 ROLE_USER 권한을 추가할 필요가 없도록 하고 싶습니다.

역할 계층을 설정하면, ROLE_ADMIN은 자동으로 ROLE_USER의 권한을 포함합니다.

2. 기존 방식의 문제점

역할 계층이 없을 경우, 권한을 처리하려면 다음 중 하나를 선택해야 합니다:
1. 모든 상위 역할에 하위 역할을 수동으로 추가

  • 예: 모든 ROLE_ADMIN 사용자에게도 ROLE_USER를 추가로 할당.
  • 역할이 많아질수록 관리가 어려워집니다.
  1. 접근 제약 조건에 상위 역할을 포함
    • 예: ROLE_USER가 필요한 모든 곳에 ROLE_ADMIN도 허용되도록 구성.
    • 역할이 많아지면 코드가 복잡해지고, 유지보수가 어렵습니다.

3. 역할 계층의 장점

1) 구성 간소화

  • 역할 간 포함 관계를 정의하면, 하위 역할에 대한 권한을 상위 역할이 자동으로 상속받습니다.
  • 예: ROLE_ADMIN > ROLE_STAFF > ROLE_USER > ROLE_GUEST
    • ROLE_ADMIN을 가진 사용자는 ROLE_GUEST까지 포함된 모든 권한을 자동으로 가집니다.

2) 코드 유지보수성 향상

  • 모든 권한을 명시적으로 지정할 필요 없이 계층 관계만 설정하면 되므로, 코드가 간결해집니다.

3) 사용자 역할 관리 효율성 증가

  • 사용자에게 여러 역할을 별도로 부여하지 않아도, 상위 역할을 부여하면 자동으로 하위 역할도 포함됩니다.

4. 역할 계층 설정 방법

4.1. 역할 계층 정의

Spring Security에서는 RoleHierarchy를 사용하여 역할 계층을 정의합니다.
RoleHierarchyImpl을 통해 계층을 설정하며, 기본적으로 ROLE_ 접두사를 사용합니다.

Java 설정
@Bean
static RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.withDefaultRolePrefix() // ROLE_ 접두사 자동 추가
        .role("ADMIN").implies("STAFF") // ROLE_ADMIN은 ROLE_STAFF 포함
        .role("STAFF").implies("USER") // ROLE_STAFF는 ROLE_USER 포함
        .role("USER").implies("GUEST") // ROLE_USER는 ROLE_GUEST 포함
        .build();
}

설명

  1. RoleHierarchyImpl:
    • 역할 계층 정의를 지원하는 클래스.
    • role("A").implies("B")A 역할이 B 역할의 권한을 포함함을 의미합니다.
  2. withDefaultRolePrefix():
    • 자동으로 ROLE_ 접두사를 추가.
    • 예: role("ADMIN")은 내부적으로 ROLE_ADMIN으로 처리.

역할 계층 예

  • ROLE_ADMIN > ROLE_STAFF > ROLE_USER > ROLE_GUEST
    • ROLE_ADMIN은 모든 하위 역할의 권한을 자동으로 상속.

4.2. 메서드 보안에서 역할 계층 사용

@PreAuthorize, @Secured, 또는 JSR-250 어노테이션(@RolesAllowed)을 사용하는 경우, 역할 계층을 메서드 보안에 적용해야 합니다.

구성 코드
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
    DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
    expressionHandler.setRoleHierarchy(roleHierarchy); // 역할 계층 설정
    return expressionHandler;
}

설명

  • MethodSecurityExpressionHandler:
    메서드 보안에서 사용되는 권한 검사 핸들러.
    역할 계층을 설정하여 상위 역할이 하위 역할의 권한을 자동으로 포함하도록 만듭니다.
적용 예: @PreAuthorize
@PreAuthorize("hasRole('USER')") 
public void userOnlyMethod() {
    // ROLE_USER 권한이 필요한 메서드
    // ROLE_ADMIN도 접근 가능 (계층 덕분)
}

4.3. HTTP 요청 보안에서 역할 계층 사용

HttpSecurity의 권한 부여 로직에서도 역할 계층이 적용됩니다.

구성 코드
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, RoleHierarchy roleHierarchy) throws Exception {
    http
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN만 접근 가능
        .requestMatchers("/user/**").hasRole("USER")  // ROLE_USER 이상 접근 가능
        .anyRequest().authenticated();
    return http.build();
}

5. 동작 원리

5.1. 역할 계층에 따른 권한 포함

  • 구성된 계층: ROLE_ADMIN > ROLE_STAFF > ROLE_USER > ROLE_GUEST
  • 사용자가 ROLE_ADMIN을 가지고 있으면:
    • 보안 제약 조건에서 ROLE_ADMIN, ROLE_STAFF, ROLE_USER, ROLE_GUEST를 모두 포함한 것처럼 동작.

5.2. 조건 평가

  1. 보안 규칙에서 hasRole("USER") 조건이 있을 경우:
    • ROLE_ADMIN, ROLE_STAFF, 또는 ROLE_USER를 가진 사용자는 조건을 만족.
  2. hasRole("GUEST") 조건이 있을 경우:
    • 모든 사용자(ADMIN, STAFF, USER, GUEST)가 조건을 만족.

6. 복잡한 요구사항 처리

복잡한 역할 매핑

  • 역할 계층은 간단한 권한 포함 관계를 처리할 수 있지만, 애플리케이션의 특정 비즈니스 요구사항에 따라 추가적인 논리가 필요할 수 있습니다.
  • 예: 특정 데이터 접근 권한이 필요하거나, 외부 시스템과 연동된 보안 요구사항.

해결 방법

  1. 역할과 권한 간 논리적 매핑 정의:

    • 역할과 접근 권한을 별도로 관리하고, 사용자 정보 로드 시 매핑하여 역할을 설정.
  2. 외부 시스템 연동:

    • 역할 계층과 함께 사용자 정의 보안 로직을 구현해 더욱 세밀한 권한 제어를 적용.

7. 요약

역할 계층의 장점

  1. 구성 단순화: 역할 포함 관계를 설정하여 중복 설정 제거.
  2. 유지보수 용이: 계층 구조만 변경하면 전체 권한 정책이 업데이트됨.
  3. 효율적인 사용자 관리: 상위 역할 하나로 하위 역할 권한까지 자동 포함.

적용 방법

  1. RoleHierarchy를 통해 계층 정의.
  2. HTTP 보안(HttpSecurity)과 메서드 보안(@PreAuthorize 등)에 적용.

예제 계층

  • ROLE_ADMIN > ROLE_STAFF > ROLE_USER > ROLE_GUEST
  • ROLE_ADMIN 사용자는 모든 하위 역할의 권한을 포함합니다.

Spring Security의 역할 계층은 복잡한 역할 기반 접근 제어를 단순화하고, 유지보수를 쉽게 만들어 줍니다.

0개의 댓글