토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차:


스프링 시큐리티의 고급 인가 기능은 가장 인기 있는 기능 중 하나다. 인증 방법과 상관없이 (스프링 시큐리티가 제공하는 메커니즘과 provider 사용 여부나, 컨테이너와의 통합 여부, 스프링 시큐리티 외의 인증 권한과의 통합 여부와는 상관없이) 어플리케이션에 일관적이고, 간단하게 인증 서비스를 적용할 수 있다.

이번 장에선 Part I에서 소개했던 AbstractSecurityInterceptor의 다양한 구현체를 살펴본다. 그다음 도메인 접근 제어 목록을 통해 인가 기능을 세부적으로 튜닝하는 방법을 살펴볼 것이다.


11.1. Authorization Architecture

11.1.1. Authorities

Authentication 섹션에선 어떻게 모든 Authentication 구현체가 GrantedAuthority 객체 리스트를 저장하는지를 설명한다. GrantedAuthority 객체는 principal에게 부여한 권한을 나타낸다. AuthenticationManagerGrantedAuthority 객체를 Authentication 객체에 삽입하며, 이후 권한을 결정할 때 AccessDecisionManagerGrantedAuthority를 읽어간다.

GrantedAuthority는 메소드가 하나 뿐인 인터페이스다:

String getAuthority();

AccessDecisionManager에선 이 메소드로 GrantedAuthority를 명확한 String으로 조회할 수 있다. GrantedAuthority가 값을 String으로 리턴하기 때문에 AccessDecisionManager 대부분이 이를 쉽게 “읽어”갈 수 있다. GrantedAuthority를 명확하게 String으로 표현할 수 없다면 GrantedAuthority는 “복잡한 케이스”로 간주하고, getAuthority()에선 null을 리턴한다.

“복잡한” GrantedAuthority의 예시로는 고객 계정 번호에 따라 적용할 작업과 권한 임계치 리스트를 저장하는 일이 있다. 이 복잡한 GrantedAuthorityString으로 표현하긴 어렵기때문에 getAuthority()null을 리턴할 것이다. null을 리턴했다는 것은 AccessDecisionManagerGrantedAuthority를 이해하기 위한 구체적인 코드가 있어야 한다는 뜻이다.

스프링 시큐리티는 한 가지 GrantedAuthority 구현체, SimpleGrantedAuthority를 제공한다. 이 클래스는 사용자가 지정한 StringGrantedAuthority로 변환해 준다. 시큐리티 아키텍처에 속한 모든 AuthenticationProviderAuthentication 객체에 값을 채울 때 SimpleGrantedAuthority를 사용한다.

11.1.2. Pre-Invocation Handling

스프링 시큐리티는 method invocation이나 웹 요청같은 보안 객체에 대한 접근을 제어하는 인터셉터를 제공한다. 호출을 허용할지 말지를 결정하는 pre-invocation 결정은 AccessDecisionManager에서 내린다.

The AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor에서 호출하며, 최종적인 접근 제어를 결정한다. AccessDecisionManager는 세 가지 메소드가 있다:

void decide(Authentication authentication, Object secureObject,
    Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

AccessDecisionManagerdecide 메소드는 권한을 결정하기 위해 필요한 모든 정보를 건내 받는다. 특히, 보안 Object를 건내 받으면 실제 보안 객체를 실행할 때 넘긴 인자를 검사할 수 있다. 예를 들어 보안 객체가 MethodInvocation이었다고 가정해보자. MethodInvocation으로 모든 Customer 인자를 쉽게 찾을 수 있으며, AccessDecisionManager 안에선 일련의 보안 로직으로 principal이 customer 관련 동작을 실행하도록 허가할 수 있다. 접근을 거절한 경우엔 AccessDeniedException을 던진다.

supports(ConfigAttribute) 메소드는 기동 시점에 AbstractSecurityInterceptor가 호출하며, AccessDecisionManager가 건내받은 ConfigAttribute의 처리 가능 여부를 결정한다. supports(Class) 메소드는 시큐리티 인터셉터 구현체가 호출하며, 설정해둔 AccessDecisionManager가 시큐리티 인터셉터가 제출할 보안 객체 타입을 지원하는지 확인한다.

Voting-Based AccessDecisionManager Implementations

인가와 관련한 모든 동작을 제어하고 싶다면 커스텀 AccessDecisionManager를 사용해도 되지만, 스프링 시큐리티는 투표를 기반으로 동작하는 몇 가지 AccessDecisionManager 구현체를 제공한다. 아래 있는 Voting Decision Manager는 관련 클래스를 도식화한 그림이다.

Figure 11. Voting Decision Manager
Voting Decision Manager

투표 방식에선 권한을 결정할 때 일련의 AccessDecisionVoter 구현체에 의견을 묻는다. 그러고나서 AccessDecisionManager가 투표 결과를 취합해서 AccessDeniedException을 던질지 말지를 결정한다.

AccessDecisionVoter는 메소드 세 개를 가진 인터페이스다:

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

구현체는 AccessDecisionVoter의 스태틱 필드 ACCESS_ABSTAIN, ACCESS_DENIED, ACCESS_GRANTED 중 하나를 의미하는 int 값을 리턴한다. 인가에 대해서 특별한 의견이 없을 때는 ACCESS_ABSTAIN을 리턴한다. 의견이 있다면 반드시 ACCESS_DENIEDACCESS_GRANTED를 리턴해야 한다.

스프링 시큐리티는 투표 결과를 집계하는 세 가지 AccessDecisionManager 구현체를 제공한다. ConsensusBased는 기권표를 제외한 투표 결과를 합산해 접근을 허가하거나 거절한다. 투표 결과가 동점이거나 모두 기권표일 때의 동작은 프로퍼티로 조절할 수 있다. AffirmativeBasedACCESS_GRANTED가 하나라도 있으면 권한을 부여한다 (i.e. 찬성표가 하나라도 있으면 거절표는 무시한다). ConsensusBased와 마찬가지로 모두 기권했을 때의 동작을 제어할 수 있는 파라미터를 제공한다. UnanimousBased는 기권을 제외한 모든 표가 만장일치로 ACCESS_GRANTED일 때만 접근을 허가한다. ACCESS_DENIED가 하나라도 있으면 접근을 거부한다. 다른 구현체와 마찬가지로 모두 기권했을 때의 동작을 제어하는 파라미터가 있다.

다른 방식으로 투표 결과를 집계하는 커스텀 AccessDecisionManager를 구현해도 된다. 예를 들어, 특정 AccessDecisionVoter의 투표에는 가중치를 두고, 특정 voter의 거절표는 기각시킬 수도 있다.

RoleVoter

스프링 시큐리티가 제공하는 AccessDecisionVoter 중 가장 많이 사용하는 건 간단한 RoleVoter다. RoleVoter는 설정 속성을 간단한 role 이름으로 취급하고, 사용자가 해당 role을 할당받았으면 접근 허가에 투표한다.

프리픽스 ROLE_로 시작하는 ConfigAttribute이 있을 때 투표에 참여한다. GrantedAuthority가 리턴하는 String이 (getAuthority() 메소드) ROLE_로 시작하는 ConfigAttributes 중 하나라도 완전히 일치하면 찬성표를 던진다. ROLE_로 시작하는 ConfigAttribute와 일치하는 게 하나도 없으면 RoleVoter는 반대표를 던진다. ROLE_로 시작하는 ConfigAttribute가 없으면 기권한다.

AuthenticatedVoter

또 다른 voter는 이전 챕터에서 살짝 언급했던 AuthenticatedVoter다. 이 voter는 익명 사용자와, 완전히 인증된 사용자와, remember-me로 인증한 사용자를 구분할 수 있다. 많은 사이트에서 일부 페이지를 remember-me로 인증한 사용자에게도 열어 놓지만, 모든 페이지에 접근하려면 로그인을 요구한다.

익명 사용자 접근을 위한 IS_AUTHENTICATED_ANONYMOUSLY 속성은 AuthenticatedVoter가 처리한다. 자세한 정보는 이 클래스의 Javadoc을 참고해라.

Custom Voters

당연히 커스텀 AccessDecisionVoter로도 원하는 곳에 접근 제어 로직을 넣을 수 있다. 보통 어플리케이션에 특화된 로직이나 (비지니스 로직 관련) 보안 관리 로직을 구현하는 데 사용한다. 예를 들어 스프링 웹사이트에 있는 블로그 문서에서 설명하는 방법으로, 실시간으로 정지된 계정의 접근을 거절할 수 있다.

11.1.3. After Invocation Handling

보안 객체를 실행하기 전에는 AbstractSecurityInterceptorAccessDecisionManager를 호출하는데, 반면에 실제로 보안 객체가 리턴하는 객체를 바꿔야 하는 어플리케이션도 있다. 이땐 직접 AOP 관심사를 구현해도 되지만, 스프링 시큐리티는 ACL 기능과 통합되는 몇 가지 구현체를 가진 편리한 훅을 제공한다.

아래 있는 After Invocation Implementation은 스프링 시큐리티의 AfterInvocationManager 인터페이스와 구현체를 도식화한 그림이다.

Figure 12. After Invocation Implementation
After Invocation Implementation

스프링 시큐리티의 설계가 대부분 그렇듯, AfterInvocationManagerAfterInvocationProviderManager라는 구현체가 하나 있으며, 이 구현체는 AfterInvocationProvider 리스트를 폴링한다. 각 AfterInvocationProvider는 리턴 객체를 수정하거나 AccessDeniedException을 던질 수 있다. 사실 여러 provider가 객체를 수정할 수 있기 때문에, provider에 전달하는 객체는 리스트에 있는 이 전 provider가 리턴한 객체다.

AfterInvocationManager를 사용하더라도, 똑같이 MethodSecurityInterceptorAccessDecisionManager가 동작을 허용하려면 설정 속성이 필요하다. AccessDecisionManager 구현체 등 전형적인 스프링 시큐리티 설정을 사용했다면, 특정 method invocation을 보호하기 위해 정의한 설정 속성이 없는 경우 모든 AccessDecisionVoter가 투표를 기권할 거다. AccessDecisionManager의 “allowIfAllAbstainDecisions” 프로퍼티가 false였다면 결국엔 AccessDeniedException을 던진다. 이 이슈를 피하려면 (1) “allowIfAllAbstainDecisions”를 true로 설정하거나 (보통 이 방법은 추천하지 않지만), (2) AccessDecisionVoter가 접근 허용에 투표할만한 설정 속성을 최소 한 개는 사용해야 한다. 후자는 (권장하는 방법) 보통 ROLE_USERROLE_AUTHENTICATED 설정 속성을 이용한다.

11.1.4. Hierarchical Roles

어떤 role은 자동으로 다른 role도 “포함”해야 한다는 건 일반적인 요구사항이다. 예를 들어 “admin” role과 “user” role이 있는 어플리케이션에선, 일반 user가 할 수 있는 일은 전부 admin도 할 수 있길 바랄 수 있다. 이를 위해선 먼저, 모든 admin 사용자에게 “user” role도 부여하는 방법이 있다. 아니면 “user” role이 필요한 모든 접근 제약 조건을 “admin” role도 포함하도록 수정해도 된다. 하지만 어플리케이션이 관리하는 role이 많다면 꽤나 복잡해진다.

role-hierarchy를 사용하면 특정 role이 (또는 권한이) 다른 role을 포함하도록 설정할 수 있다. 스프링 시큐리티의 RoleVoter를 확장한 RoleHierarchyVoterRoleHierarchy를 설정할 수 있으며, 이 값을 통해 사용자에게 할당할 모든 “reachable authorities”를 가져올 수 있다. 전형적인 설정은 다음과 같다:

<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
    <constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy">
        <value>
            ROLE_ADMIN > ROLE_STAFF
            ROLE_STAFF > ROLE_USER
            ROLE_USER > ROLE_GUEST
        </value>
    </property>
</bean>

여기서 사용한 계층 구조에는 네 가지 역할이 있다 (ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST). AccessDecisionManager에 위 RoleHierarchyVoter를 설정하면 제약 조건을 평가할 때, ROLE_ADMIN으로 인증한 사용자는 마치 네 가지 role을 모두 가진 것처럼 행동할 수 있다. > 부호는 “포함한다”로 해석하면 된다.

Role hierarchy는 어플리케이션의 접근 제어 설정을 단순화하고, 사용자마다 할당할 권한을 줄일 수 있는 편리한 수단이다. 좀 더 복잡한 요구사항이 있다면 어플리케이션에 필요한 접근 권한과 사용자에게 할당할 role을 매핑하는 로직을 정의해서 사용자 정보를 로드할 때 이 둘을 변환하면 된다.


11.2. Authorize HttpServletRequest with FilterSecurityInterceptor

이번 섹션은 서블릿 아키텍처와 구현체를 기반으로 서블릿 기반 어플리케이션에서 권한을 부여하는 방법을 자세히 설명한다.

FilterSecurityInterceptorHttpServletRequest를 사용해서 권한을 인가한다. 이 인터셉터는 FilterChainProxy에 하나의 보안 필터로 추가된다.

Authorize HttpServletRequest

기본적으로 스프링 시큐리티에서 권한을 인가하려면 모든 요청을 인증해야 한다. 다음 코드로 설정을 명시할 수도 있다:

Example 78. Every Request Must be Authenticated

java xml kotlin
protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        );
}
<http>
    <!-- ... -->
    <intercept-url pattern="/**" access="authenticated"/>
</http>
fun configure(http: HttpSecurity) {
    http {
        // ...
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
    }
}

여러 가지 규칙에 우선순위를 부여할 수도 있다:

Example 79. Authorize Requests

java xml kotlin
protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize // (1)
            .mvcMatchers("/resources/**", "/signup", "/about").permitAll() // (2)
            .mvcMatchers("/admin/**").hasRole("ADMIN") // (3)
            .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // (4)
            .anyRequest().denyAll() // (5)         
        );
}
<http> <!-- (1) -->
    <!-- ... -->
    <!-- (2) -->
    <intercept-url pattern="/resources/**" access="permitAll"/>
    <intercept-url pattern="/signup" access="permitAll"/>
    <intercept-url pattern="/about" access="permitAll"/>

    <intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> <!-- (3) -->
    <intercept-url pattern="/db/**" access="hasRole('ADMIN') and hasRole('DBA')"/> <!-- (4) -->
    <intercept-url pattern="/**" access="denyAll"/> <!-- (5) -->
</http>
fun configure(http: HttpSecurity) {
   http {
        authorizeRequests { // (1)
            authorize("/resources/**", permitAll) // (2)
            authorize("/signup", permitAll)
            authorize("/about", permitAll)

            authorize("/admin/**", hasRole("ADMIN")) // (3)
            authorize("/db/**", "hasRole('ADMIN') and hasRole('DBA')") // (4)
            authorize(anyRequest, denyAll) // (5)
        }
    }
}

(1) 인가 조건을 여러 개 지정했다. 각 규칙은 선언한 순서대로 적용된다.
(2) 모든 사용자가 접근할 수 있는 몇 가지 URL 패턴을 지정한다. 구체적으로 말하면 “/resources/”로 시작하는 URL이나, “/signup”, “/about” 요청은 모든 사용자가 접근할 수 있다.
(3) “/admin/”으로 시작하는 모든 요청은 “ROLE_ADMIN”을 가진 사용자로 제한한다. hasRole 메소드를 사용했기 때문에 “ROLE_” 프리픽스는 지정할 필요 없다.
(4) ”/db/”로 시작하는 모든 요청은 “ROLE_ADMIN”과 “ROLE_DBA” 모두 필요하다. hasRole 메소드를 사용했기 때문에 “ROLE_” 프리픽스는 지정할 필요 없다.
(5) 위 조건에 충족하지 않는 다른 URL은 접근을 거절한다. 인증 조건을 누락하는 실수를 방지하는 좋은 전략이다.


11.3. Expression-Based Access Control

스프링 시큐리티 3.0부터 간단한 설정 속성과 voter로 접근 권한을 결정하는 방법 외에도, 스프링 EL 표현식을 사용해 인가 메커니즘을 구현할 수 있다. 표현식 기반으로 접근을 제어할 땐 동일한 아키텍처를 사용하지만, 복잡한 Boolean 로직을 간단한 표현식 하나로 캡슐화할 수 있다.

11.3.1. Overview

스프링 시큐리티는 스프링 EL 표현식을 사용한다. 이 주제를 자세히 알고싶다면 EL 표현식의 동작 방식을 살펴보는게 좋다. 표현식을 평가할 땐 평가 컨텍스트의 일부로 “루트 객체”를 사용한다. 스프링 시큐리티는 웹과 메소드 시큐리티 전용 클래스를 루트 객체로 사용하기 때문에, 별도의 내장 표현식을 사용할 수 있으며 현재 principal 등에 접근할 수 있다.

Common Built-In Expressions

표현식 루트 객체를 위한 베이스 클래스는 SecurityExpressionRoot다. 이 클래스는 웹, 메소드 시큐리티에서 공통적으로 사용할 수 있는 공통 표현식을 몇 가지 제공한다.

Table 1. Common built-in expressions

Expression Description
hasRole(String role) 현재 principal이 명시한 role을 가지고 있으면 true를 리턴한다.

예를 들어, hasRole('admin')

기본적으로 파라미터로 넘긴 role이 ‘ROLE_‘로 시작하지 않으면 이 프리픽스를 추가한다. DefaultWebSecurityExpressionHandlerdefaultRolePrefix를 수정하면 커스텀할 수 있다.
hasAnyRole(String… roles) 현재 principal이 명시한 role 중 하나라도 가지고 있으면 true를 리턴한다 (문자열 리스트를 콤마로 구분해서 전달한다).

예를 들어 hasAnyRole('admin', 'user')

기본적으로 파라미터로 넘긴 role이 ‘ROLE_‘로 시작하지 않으면 이 프리픽스를 추가한다. DefaultWebSecurityExpressionHandlerdefaultRolePrefix를 수정하면 커스텀할 수 있다.
hasAuthority(String authority) 현재 principal이 명시한 권한이 있으면 true를 리턴한다.

예를 들어 hasAuthority('read')
hasAnyAuthority(String… authorities) 현재 principal이 명시한 권한 중 하나라도 있으면 true를 리턴한다 (문자열 리스트를 콤마로 구분해서 전달한다).

예를 들어 hasAnyAuthority('read', 'write')
principal 현재 사용자를 나타내는 principal 객체에 직접 접근할 수 있다.
authentication SecurityContext로 조회할 수 있는 현재 Authentication 객체에 직접 접근할 수 있다.
permitAll 항상 true로 평가한다.
denyAll 항상 false로 평가한다.
isAnonymous() 현재 principal이 익명 사용자면 true를 리턴한다.
isRememberMe() 현재 principal이 remember-me 사용자면 true를 리턴한다.
isAuthenticated() 사용자가 익명이 아니면 true를 리턴한다.
isFullyAuthenticated() 사용자가 익명 사용자나 remember-me 사용자가 아니면 true를 리턴한다.
hasPermission(Object target, Object permission) 사용자가 target에 해당 permission 권한이 있으면 true를 리턴한다. 예를 들어 hasPermission(domainObject, 'read')
hasPermission(Object targetId, String targetType, Object permission) 사용자가 target에 해당 permission 권한이 있으면 true를 리턴한다. 예를 들어 hasPermission(1, 'com.example.domain.Message', 'read')

11.3.2. Web Security Expressions

URL별로 표현식을 적용하려면 먼저 <http> 요소의 use-expressions 속성을 true로 설정해야 한다. 이렇게 하면 스프링 시큐리티는 <intercept-url> 요소의 access 속성에 스프링 EL 표현식이 있음을 인지할 수 있다. 표현식은 접근을 허용할지 말지를 Boolean으로 평가해야 한다. 예를 들어:

<http>
    <intercept-url pattern="/admin*"
        access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
    ...
</http>

여기서는 어플리케이션의 “admin” 영역은 (URL 패턴으로 정의함) “admin” 권한을 부여받은 사용자가 로컬 서브넷으로 접근할 때만 사용할 수 있도록 정의했다. 내장된 hasRole 표현식은 이미 이전 섹션에서 살펴봤다. hasIpAddress 표현식은 웹 보안에서 사용할 수 있는 또다른 내장 표현식이다. WebSecurityExpressionRoot 클래스에 정의돼 있으다. 웹 접근 표현식을 평가할 땐 이 클래스 인스턴스를 루트 객체로 사용한다. 또한 이 객체는 HttpServletRequest 객체를 request란 이름으로 직접 노출하므로 표현식에서 직접 request를 호출할 수도 있다. 표현식을 사용하면 네임스페이스에 있는 AccessDecisionManagerWebExpressionVoter가 추가된다. 따라서 네임스페이스 없이 표현식을 사용한다면 이 중 하나를 설정에 직접 추가해야 한다.

Referring to Beans in Web Security Expressions

사용할 수 있는 표현식을 늘리고 싶다면, 어떤 객체든지 스프링 빈으로 정의하면 쉽게 참조할 수 있다. 예를 들어 webSecurity란 이름의 빈이 있고, 이 빈의 메소드 시그니처는 아래와 같다고 가정해보자:

public class WebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
                ...
        }
}

이 메소드는 다음과 같이 참조할 수 있다:

<http>
    <intercept-url pattern="/user/**"
        access="@webSecurity.check(authentication,request)"/>
    ...
</http>

또는 자바 설정을 사용한다면

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
        ...
    )

Path Variables in Web Security Expressions

URL에 있는 path variable을 참조해야 할 때도 있다. 예를 들어 /user/{userId} 형식의 URL path에서 id를 가져와 사용자를 검색하는 RESTful 어플리케이션을 생각해 보자.

path variable도 간단하게 패턴에 지정해서 참조할 수 있다. 예를 들어 webSecurity란 이름의 빈이 있고, 이 빈의 메소드 시그니처는 아래와 같다고 가정해보자:

public class WebSecurity {
        public boolean checkUserId(Authentication authentication, int id) {
                ...
        }
}

이 메소드는 다음과 같이 참조할 수 있다:

<http>
    <intercept-url pattern="/user/{userId}/**"
        access="@webSecurity.checkUserId(authentication,#userId)"/>
    ...
</http>

또는 자바 설정을 사용한다면

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
        ...
    );

두 설정 모두, URL이 매칭되면 checkUserId 메소드로 path variable을 (변환까지 해서) 전달한다. 예를 들어 URL이 /user/123/resource였다면 전달하는 id는 123이다.

11.3.3. Method Security Expressions

메소드 시큐리티는 단순한 허가 또는 거절보다 조금 더 복잡한 규칙을 사용한다. 스프링 시큐리티 3.0은 표현식을 종합적으로 지원하기 위한 새 애노테이션을 도입했다.

@Pre and @Post Annotations

네 가지 애노테이션이 표현식 속성을 지원한다. 사전/사후 권한 체크를 지원하며, 제출한 컬렉션 인자나 리턴한 값을 필터링할 수도 있다. 이 애노테이션은 바로 @PreAuthorize, @PreFilter, @PostAuthorize, @PostFilter다. 애노테이션 사용은 네임스페이스의 global-method-security 요소로 활성화한다.

<global-method-security pre-post-annotations="enabled"/>

Access Control using @PreAuthorize and @PostAuthorize

가장 유용할 애노테이션은 실제로 메소드를 실행할 수 있는지 없는지를 결정하는 @PreAuthorize다. 예를 들어 (“contacts” 샘플 어플리케이션 코드 일부 발췌)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

이 애노테이션은 “ROLE_USER” 권한이 있는 사용자만 접근할 수 있다는 뜻이다. 물론 전통적인 설정과 간단한 설정 속성으로도 동일한 권한 조건을 쉽게 구성할 수 있다. 하지만 이건 어떨까:

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

여기선 현재 사용자가 실제로 주어진 연락처에 “admin” 권한이 있는지를 확인하기 위해 메소드 인자를 표현식 일부로 사용하고 있다. 내장 표현식 hasPermission()은 어플리케이션 컨텍스트를 통해 스프링 시큐리티의 ACL 모듈로 연결되며, 아래에서 설명한다. 메소드 인자는 인자 이름을 기준으로 표현식 변수로 사용할 수 있다.

스프링 시큐리티가 메소드 인자를 리졸브하는 방법은 여러 가지가 있다. 스프링 시큐리티는 DefaultSecurityParameterNameDiscoverer를 사용해서 파라미터 이름을 발견한다. 디폴트로 메소드 전체에 대해 다음 옵션을 시도해 본다.

표현식 내에선 모든 스프링 EL 기능을 사용할 수 있으므로 인자의 프로퍼티에도 접근할 수 있다. 예를 들어 특정 메소드를, username이 연락처의 이름과 일치하는 사용자에게만 허가하고 싶다면 다음과 같이 작성할 수 있다.

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

여기선 또 다른 내장 표현식 authentication을 사용했으며, 이는 보안 컨텍스트에 저장된 Authentication을 나타낸다. principal 표현식을 사용하면 “principal” 프로퍼티에 직접 접근할 수도 있다. principal은 보통 UserDetails 인스턴스이므로 principal.username이나 principal.enabled같은 표현식을 사용하면 된다.

일반적이진 않지만, 메소드를 실행한 다음에 접근 제어를 확인하고 싶을 수도 있다. 이땐 @PostAuthorize 애노테이션을 사용하면 된다. 메소드가 리턴한 값에 접근하려면 표현식에 내장된 이름 returnObject를 사용해라.

Filtering using @PreFilter and @PostFilter

이미 알고 있겠지만, 스프링 시큐리티는 컬렉션이나 배열 필터링을 지원하며, 이제는 표현식으로 구현할 수 있다. 메소드가 리턴한 값에 가장 많이 사용한다. 예를 들어:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();

@PostFilter 애노테이션을 사용하면, 스프링 시큐리티는 리턴된 컬렉션을 순회해서 표현식 결과가 false인 모든 요소를 제거한다. filterObject는 컬렉션의 현재 객체를 참조한다. @PreFilter를 사용하면 메소드 호출 전에 필터링할 수도 있지만, 흔한 요구사항은 아니다. 기본 문법은 동일하지만, 컬렉션 타입 인자가 둘 이상이라면 애노테이션의 filterTarget 프로퍼티에 인자 이름을 지정해야 한다.

필터링은 데이터 조회 쿼리를 튜닝하는 용도가 아니라는 점을 명심해야 한다. 사이즈가 큰 컬렉션을 필터링하고 많은 엔트리를 제거하는 것은 비효율적이다.

Built-In Expressions

시큐리티에 특화된 내장 표현식도 있다. 사실 위에서 이미 다뤘다. filterTargetreturnValue도 간단하게 사용할 수 있지만, hasPermission() 표현식을 사용하면 좀 더 자세히 살펴볼 수도 있다.

The PermissionEvaluator interface

hasPermission() 표현식은 PermissionEvaluator 인스턴스로 위임된다. 이 인터페이스는 표현식 시스템과 스프링 시큐리티의 ACL 시스템을 연결하기 위한 것으로, 추상적인 permission 기반으로 도메인 객체에 인가 조건을 지정할 수 있다. ACL 모듈에 직접적인 의존성은 없으므로 필요하다면 다른 구현체로 바꿀 수 있다. 이 인터페이스엔 두 가지 메소드가 있다:

boolean hasPermission(Authentication authentication, Object targetDomainObject,
                            Object permission);

boolean hasPermission(Authentication authentication, Serializable targetId,
                            String targetType, Object permission);

첫 번째 인자를 (Authentication 객체) 제공하지 않은 경우만 제외하면 모두 가능한 표현식으로 매핑된다. 첫 번째 메소드는 접근을 제어하는 도메인 객체를 이미 로드한 경우에 사용한다. 현재 사용자가 이 객체에 주어진 permission이 있다면 표현식은 true를 리턴한다. 두 번째 메소드는 객체를 로드하진 않았지만 그 식별자를 알 때 사용한다. 올바른 ACL permission을 로드하려면 도메인 객체에 대한 추상적인 “type” 지정자도 필요하다. 보통은 도메인 객체의 자바 클래스를 사용하지만, permission을 로드하는 방식과 일치하기만 하면 꼭 그래야 한다는 법은 없다.

hasPermission() 표현식을 사용하려면 어플리케이션 컨텍스트에 PermissionEvaluator를 명시해야 한다. 예를 들어 다음과 같다:

<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler ref="expressionHandler"/>
</security:global-method-security>

<bean id="expressionHandler" class=
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
    <property name="permissionEvaluator" ref="myPermissionEvaluator"/>
</bean>

myPermissionEvaluatorPermissionEvaluator를 구현한 빈이다. 보통 ACL 모듈에 있는 구현체 AclPermissionEvaluator를 사용한다. 자세한 내용은 “Contacts” 샘플 어플리케이션 설정을 참고해라.

Method Security Meta Annotations

메소드 시큐리티에 메타 애노테이션을 사용하면 코드를 좀 더 가독성있는 코드로 만들 수 있다. 똑같은 복잡한 표현식을 코드 전체에 반복하고 있다면 특히 유용할 것이다. 예를 들어 아래 코드를 생각해 보자:

@PreAuthorize("#contact.name == authentication.name")

이 코드를 여기저기 반복하는 대신, 이 코드 대신 사용할 메타 애노테이션을 만들 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

메타 애노테이션은 스프링 시큐리티의 모든 메소드 시큐리티 애노테이션에 사용할 수 있다. 스펙 준수를 위해 JSR-250 애노테이션은 메타 애노테이션을 지원하지 않는다.


11.4. Secure Object Implementations

11.4.1. AOP Alliance (MethodInvocation) Security Interceptor

스프링 시큐리티 2.0 이전엔 MethodInvocation을 보호하려면 꽤 많은 보일러플레이트 설정이 필요했다. 현재 권장하는 메소드 시큐리티 설정 방법은 네임스페이스 설정이다. 네임스페이스를 사용하면 메소드 시큐리티를 위한 빈들이 자동으로 설정되기 때문에 정말로 구현체를 알 필요가 없다. 여기선 관련 클래스 개요만 간단하게 짚고 넘어간다.

메소드 시큐리티는 MethodInvocation을 보호해 주는 MethodSecurityInterceptor로 구현한다. 설정 방법에 따라 인터셉터를 특정한 빈 하나에서만 사용할 수도 있고, 인터셉터 하나를 여러 빈이 공유할 수도 있다. 인터셉터는 MethodSecurityMetadataSource 인스턴스를 사용해서 특정 method invocation에 적용할 설정 속성을 가져온다. MapBasedMethodSecurityMetadataSource로는 메소드 이름을 (와일드카드 지원) 키로 갖는 설정 속성을 저장하며, 내부적으로 <intercept-methods><protect-point> 요소로 어플리케이션에 해당 속성을 정의했을 때 사용한다. 다른 구현체는 애노테이션 기반 설정에서 사용한다.

Explicit MethodSecurityInterceptor Configuration

물론 어플리케이션 컨텍스트에 MethodSecurityInterceptor를 직접 설정해서 스프링 AOP의 프록시 메커니즘과 함께 사용할 수도 있다:

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

11.4.2. AspectJ (JoinPoint) Security Interceptor

AspectJ 보안 인터셉터는 앞에서 설명한 AOP Alliance 보안 인터셉터와 매우 유사하다. 실제로 이번 섹션에선 차이점만 다뤄볼 것이다.

이 AspectJ 인터셉터 이름은 AspectJSecurityInterceptor다. 프록시를 통해 인터셉터를 구성할 때 스프링 어플리케이션 컨텍스트에 의존하는 AOP Alliance 보안 인터셉터와는 달리, AspectJSecurityInterceptor는 AspectJ 컴파일러를 통해 구성된다. 어플리케이션 하나에 보안 인터셉터 두 종류를 모두 사용하는 게 그렇게 드문 일도 아니다. 보통 AspectJSecurityInterceptor로 도메인 객체 인스턴스 보안을 처리하고, AOP Alliance MethodSecurityInterceptor로 서비스 레이어 보안을 처리한다.

먼저 스프링 어플리케이션 컨텍스트에 AspectJSecurityInterceptor를 설정하는 방법을 알아보자:

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

보이는 바와 같이 클래스 명만 제외하면 AspectJSecurityInterceptor는 AOP Alliance 보안 인터셉터와 완전히 동일하다. 사실 SecurityMetadataSource는 AOP 라이브러리에 있는 클래스가 아닌 java.lang.reflect.Method로 동작하기 때문에, 두 인터셉터에 같은 securityMetadataSource를 공유하는 것도 가능하다. 물론 접근 권한을 결정할 땐 관련 AOP 라이브러리 전용 invocation을 (ie MethodInvocation 또는 JoinPoint) 사용하기 때문에, 다양한 추가 기준도 고려해서 결정할 수 있다 (메소드 인자 등).

다음은 AspectJ aspect를 정의해야 한다. 예를 들어:

package org.springframework.security.samples.aspectj;

import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;

public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {

    private AspectJSecurityInterceptor securityInterceptor;

    pointcut domainObjectInstanceExecution(): target(PersistableEntity)
        && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);

    Object around(): domainObjectInstanceExecution() {
        if (this.securityInterceptor == null) {
            return proceed();
        }

        AspectJCallback callback = new AspectJCallback() {
            public Object proceedWithObject() {
                return proceed();
            }
        };

        return this.securityInterceptor.invoke(thisJoinPoint, callback);
    }

    public AspectJSecurityInterceptor getSecurityInterceptor() {
        return securityInterceptor;
    }

    public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
        this.securityInterceptor = securityInterceptor;
    }

    public void afterPropertiesSet() throws Exception {
        if (this.securityInterceptor == null)
            throw new IllegalArgumentException("securityInterceptor required");
        }
    }
}

위 예시에선 보안 인터셉터는 모든 PersistableEntity 인스턴스에 적용되며, PersistableEntity는 위에 나타나있지 않지만 추상 클래스다. (원하는 다른 클래스나 pointcut 표현식을 사용해도 된다). 궁금할까봐 말하자면, AspectJCallbackproceed(); 구문은 around() 본문 내에 있을 때만 특별한 의미를 갖기 때문에 필요하다. AspectJSecurityInterceptor는 타겟 객체를 계속 이어가려면 이 익명 AspectJCallback 클래스를 실행한다.

스프링이 이 aspect를 로드하고 AspectJSecurityInterceptor와 연결할 수 있도록 설정을 추가해야 한다. 이를 위한 빈 정의는 아래에 있다:

<bean id="domainObjectInstanceSecurityAspect"
    class="security.samples.aspectj.DomainObjectInstanceSecurityAspect"
    factory-method="aspectOf">
<property name="securityInterceptor" ref="bankManagerSecurity"/>
</bean>

이게 전부다! 이제 어플리케이션 내 어디든지 적합하다고 생각하는 방법으로 (eg. new Person();) 빈을 만들 수 있으며, 그 빈에는 시큐리티 인터셉터가 적용될 거다.


11.5. Method Security

2.0 버전 이후 스프링 시큐리티는 서비스 레이어 메소드를 보호하기 위한 기능을 대폭 개선했다. 프레임워크의 기존 @Secured 애노테이션 외에 JSR-250 애노테이션도 지원한다. 3.0부터는 새로운 표현식 기반 애노테이션도 사용할 수 있다. 원하는 빈을 선언한 곳을 intercept-methods 요소로 장식하면 단일 빈을 보호할 수도 있고, AspectJ 스타일 포인트컷으로 서비스 레이어 전체에 걸친 빈을 보호할 수도 있다.

11.5.1. EnableGlobalMethodSecurity

@Configuration 인스턴스 중 아무곳에나 @EnableGlobalMethodSecurity 애노테이션을 붙이면 애노테이션 기반 보안 기능을 활성화할 수 있다. 예를 들어 아래 코드는 스프링 시큐리티의 @Secured 애노테이션을 활성화한다.

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

메소드에 (클래스나 인터페이스에 있는) 애노테이션을 달면 이제 해당 메소드의 조건에 따라 접근을 제한할 것이다. 스프링 시큐리티의 네이티브 애노테이션은 메소드의 속성 셋을 정의한다. 이 값은 실제 결정을 내리는 AccessDecisionManager로 전달된다:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

JSR-250 애노테이션은 다음과 같이 활성화할 수 있다.

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

이는 표준을 따르며 간단한 role기반 제약 조건을 적용할 순 있지만, 스프링 시큐리티의 네이티브 애노테이션같은 기능은 없다. 새로운 표현식 기반 문법을 사용하려면 다음과 같이 작성해야 한다.

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

위와 동일한 자바 코드는 다음과 같다:

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

11.5.2. GlobalMethodSecurityConfiguration

@EnableGlobalMethodSecurity 애노테이션이 지원하는 것보다 더 복잡한 작업이 필요할 때도 있을 것이다. 이런 인스턴스는 GlobalMethodSecurityConfiguration을 확장해서 하위 클래스에도 @EnableGlobalMethodSecurity 애노테이션을 달아주면 된다. 예를 들어 커스텀 MethodSecurityExpressionHandler를 사용하고 싶다면 아래 설정을 사용할 수 있다:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        // ... create and return custom MethodSecurityExpressionHandler ...
        return expressionHandler;
    }
}

재정의할 수 있는 메소드에 대한 자세한 정보는 GlobalMethodSecurityConfiguration Javadoc을 참고해라.

11.5.3. The <global-method-security> Element

이 요소는 애노테이션 기반 보안을 활성화하며 (요소에 적절한 속성을 설정해서), 어플리케이션 컨텍스트 전역에 적용할 포인트컷 선언을 함께 묶을 때도 사용한다. <global-method-security> 요소 하나만 선언하면 된다. 아래 선언문은 스프링 시큐리티의 @Secured 지원을 활성화한다:

<global-method-security secured-annotations="enabled" />

메소드에 (클래스나 인터페이스에 있는) 애노테이션을 달면 이제 해당 메소드의 조건에 따라 접근을 제한할 것이다. 스프링 시큐리티의 네이티브 애노테이션은 메소드의 속성 셋을 정의한다. 이 값은 실제 결정을 내리는 AccessDecisionManager로 전달된다:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

JSR-250 애노테이션은 다음과 같이 활성화할 수 있다.

<global-method-security jsr250-annotations="enabled" />

이는 표준을 따르며 간단한 role기반 제약 조건을 적용할 순 있지만, 스프링 시큐리티의 네이티브 애노테이션같은 기능은 없다. 새로운 표현식 기반 문법을 사용하려면 다음과 같이 작성해야 한다.

<global-method-security pre-post-annotations="enabled" />

위와 동일한 자바 코드는 다음과 같다:

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

사용자의 권한 리스트로 role 이름을 확인하는 것 외에 다른 규칙을 간단하게 정의해야 한다면 표현식 기반 애노테이션을 사용하는 게 좋다.

애노테이션을 붙인 메소드는 스프링 빈으로 정의한 (메소드 시큐리티가 활성화된 어플리케이션 컨텍스트 내에 있는) 인스턴스일 때만 보안이 적용된다. 스프링이 생성하지 않는 인스턴스를 (예를 들어 new 연산자로 생성한 인스턴스) 보호하고 싶다면 AspectJ를 사용해야 한다.

어플리케이션 내에선 애노테이션 유형을 여러 개 활성화해도 되지만, 특정 인터페이스나 클래스에는 한 가지 유형만 적용해야 한다. 그렇지 않으면 의도한 대로 정의되지 않을 것이다. 메소드 하나에서 두 애노테이션을 발견하면 둘 중 하나만 적용한다.

11.5.4. Adding Security Pointcuts using protect-pointcut

protect-pointcut은 좀 더 강력한 기능을 제공하는데, 간단한 선언문 하나만으로도 많은 빈을 보호할 수 있다. 다음 예제를 생각해 보자:

<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
    access="ROLE_USER"/>
</global-method-security>

여기선 어플리케이션 컨텍스트에 선언한 빈 중 com.mycompany 패키지에 있으며 “Service”로 끝나는 클래스의 모든 메소드를 보호한다. ROLE_USER role이 있는 사용자만 이 메소드를 실행할 수 있다. URL 매칭과 마찬가지로, 첫 번째로 매칭되는 표현식을 사용하기 때문에 가장 구체적인 조건이 앞에 있어야 한다. 시큐리티 애노테이션이 포인트컷보다 우선시된다.


11.6. Domain Object Security (ACLs)

11.6.1. Overview

복잡한 어플리케이션은 단순히 웹 요청이나 메소드 실행 단위로 접근 권한을 관리할 수 없을 때도 있다. 대신에 누가 (Authentication), 언제 (MethodInvocation) 무엇을 (SomeDomainObject) 하는가를 전부 고려해야 한다. 다시 말해 메소드를 실행하는 실제 도메인 객체 인스턴를 고려해서 인가 여부를 결정해야 한다.

동물 병원에서 필요한 어플리케이션을 설계한다고 생각해보자. 스프링 기반 어플리케이션에서 사용할 사용자는 크게 동물 병원 직원과 손님으로 나뉜다. 직원은 모든 데이터에 접근할 수 있지만 손님은 본인의 기록만 볼 수 있다. 조금 더 재미 있게, 손님은 “강아지 유치원” 멘토나 지역 “포니 클럽” 회장같은 자신의 기록을 다른 사용자도 볼 수 있게 만들 수 있다고 해보자. 스프링 시큐리티를 기반으로 만든다면 고려해볼 수 있는 옵션이 몇 가지 있다:

여기 있는 해결책 모두 정당한 방법이다. 하지만 첫 번째 방법은 인가 권한 확인 로직과 비지니스 코드의 결합도가 올라간다. 이로 인한 주요 문제점은 단위 테스트가 어려워지며, Customer 인가 로직을 어디에도 재사용하기 어렵다는 것이다. Authentication 객체에서 GrantedAuthority[]를 가져오는 것도 괜찮지만, Customer가 많아지면 확장하기 어렵다. 사용자가 5000명의 Customer에 접근할 수 있다면 (불가능할 것 같지만, 대형 포니 클럽의 유명한 수의사라고 생각해보라!), Authentication 객체를 구성하는데 그만큼의 메모리와 시간이 필요하며, 이는 결코 바람직한 일이 아니다. 세 번째 방법은 외부 코드에서 직접 Customer를 열기 때문에 이 셋 중엔 가장 나은 방법처럼 보인다. 관심사를 분리했고, 메모리나 CPU 사이클을 낭비하진 않지만, AccessDecisionVoter와 마지막 비지니스 메소드 자체에서 Customer 객체를 가져오는 DAO를 호출한다는 점은 여전히 비효율적이다. 메소드를 호출할 때마다 두번씩 접근하는 건 분명히 바람직하지 않다. 게다가 여기 있는 모든 방법은 접근 제어 리스트(access control list, ACL) 저장 로직과 비지니스 로직을 직접 처음부터 작성해야 한다.

다행히도 다른 방법이 하나 더 있는데, 아래에서 이야기할 것이다.

11.6.2. Key Concepts

스프링 시큐리티의 ACL 서비스는 spring-security-acl-xxx.jar에 실려있다. 스프링 시큐리티의 도메인 객체 인스턴스 보안 기능을 사용하려면 이 JAR를 클래스패스에 포함시켜야 한다.

스프링 시큐리티의 도메인 객체 인스턴스 보안 기능의 중심은 바로 접근 제어 목록(ACL)에 있다. 시스템 내 에 있는 모든 도메인 객체 인스턴스는 자신의 ACL을 가지며, ACL은 누가 그 객체에 접근할 수 있고 없는지에 대한 정보를 가지고 있다. 이를 염두에 두고, 이제 스프링 시큐리티가 제공하는 세 가지 핵심 ACL 관련 기능을 살펴보자:

첫 번째 기능에서 알 수 있듯이, 스프링 시큐리티 ACL 모듈의 핵심 기능 중 하나는 ACL을 빠르게 조회하는 것이다. 이 ACL 저장소 기능은 매우 중요한 것 중 하나인데, 모든 도메인 객체 인스턴스는 접근 제어 엔트리를 여러 개 가질 것이고, 각 ACL은 트리같은 구조로 다른 ACL들을 상속기 때문에 그렇다 (매우 흔하게 사용하는 패턴이고, 스프링 시큐리티도 기본적으로 지원한다). 스프링 시큐리티의 ACL 기능은 고성능 ACL 검색 외에도, 플러그인처럼 쉽게 사용할 수 있는 캐시, 데드락을 최소화한 데이터베이스 업데이트, ORM 프레임워크와의 독립성 (직접 JDBC를 사용한다), 적절한 캡슐화, 투명한 데이터베이스 업데이트를 함께 고려해서 설계했다.

데이터베이스가 ACL 모듈 작동의 중심에 있으므로, 구현체에서 디폴트로 사용하는 네 가지 메인 테이블을 살펴보겠다. 다음은 전형적인 스프링 시큐리티 ACL에서 사용하는 테이블이며, 밑으로 갈수록 로우(row)가 많은 테이블이다:

마지막 테이블에서 언급했지만, ACL 시스템은 정수 비트 마스킹을 사용한다. ACL 시스템을 사용한다고 해서 비트 시프트를 잘 알아야 하는 것은 아니므로 걱정하지 않아도 된다. 32비트를 끄고 킨다는 게 뭔지만 이해한다면 충분하다. 각 비트는 permission을 나타내며, 기본적으로 0비트는 read, 1비트는 write, 2비트는 create, 3비트는 delete, 4비트는 관리자 permission을 의미한다. 다른 permission을 사용하고 싶으면 쉽게 자체 Permission 인스턴스를 구현할 수 있으며, 나머지 ACL 프레임워크 코드는 해당 구현체를 알지 못해도 잘 동작할 것이다.

시스템 내에 있는 도메인 객체 수는 정수 비트 마스크를 사용하기로 한 것과는 아무 상관 없다는 것을 이해해야 한다. permission은 32비트로 표현할 수 있지만, 도메인 객체는 수십억 개가 있을 수도 있다 (ACL_OBJECT_IDENTITY와 ACL_ENTRY 로우가 수십억개가 될 수 있다는 뜻이다). 도메인 객체마다 비트가 하나씩 필요하다고 오해하는 사람들이 가끔 있어서 짚고 넘어가는데, 이는 잘못된 생각이다.

ACL 시스템이 하는 일과 테이블 구조에 대해 기본적인 내용은 설명했으므로, 이제 핵심 인터페이스를 설명한다. 핵심 인터페이스는 다음과 같다:

즉시 사용할 수 있는 AclService와 관련 데이터베이스 클래스는 모두 ANSI SQL을 사용한다는 점에 주의해라. 따라서 주요 데이터베이스에서는 모두 동작할 것이다. 이 글을 쓰는 시점에는 Hypersonic SQL, PostgreSQL, Microsoft SQL Server, Oracle로 테스트를 완료했다.

스프링 시큐리티는 ACL 모듈을 사용하는 두 가지 샘플을 제공한다. Contacts 샘플과 Document Management System (DMS) 샘플이다. 이 샘플을 살펴보길 권한다.

11.6.3. Getting Started

스프링 시큐리티의 ACL 기능을 사용하려면, 어딘가엔 ACL 정보를 저장해야 한다. 즉 스프링을 사용하는 DataSource 인스턴스가 필요하다. 그러면 DataSourceJdbcMutableAclService, BasicLookupStrategy 인스턴스에 주입한다. 후자는 고성능 ACL 검색 기능, 전자는 수정 기능을 제공한다. 설정 예시는 스프링 시큐리티가 함께 제공하는 샘플 중 하나를 참고하라. 또한 마지막 섹션에 있는 ACL 관련 테이블 4개를 데이터베이스에 추가해야 한다 (적당한 SQL 문은 ACL 샘플 참고).

필요한 스키마와 JdbcMutableAclService 인스턴스를 만들었다면, 도메인 모델이 스프링 시큐리티 ACL 패키지와 호환되는지를 확인해야 한다. ObjectIdentityImpl은 다양하게 사용할 수 있으므로, 아마 이것 만으로 충분할 것이다. 도메인 객체엔 대부분 public Serializable getId() 메소드가 있을 것이다. 리턴 타입이 long이거나 long과 호환되는 경우엔 (eg int), ObjectIdentity 문제는 더 이상 생각하지 않아도 된다. ACL 모듈은 많은 곳에서 long 식별자를 사용한다. long (또는 int, byte 등)을 사용하지 않는 다면 클래스를 대량 다시 구현해야 할 가능성이 크다. long은 이미 모든 데이터베이스 시퀀스와 호환되고, 가장 많이 사용하는 식별자 데이터 타입이며, 일반적인 사용 시나리오에서 전부 수용할 수 있는 길이이므로, 스프링 시큐리티의 ACL 모듈은 long 이외의 식별자를 지원하지 않는다.

다음 코드는 Acl을 만들고 기존 Acl을 수정하는 방법을 보여준다:

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

위 예제에선 식별 숫자가 44인 “Foo” 도메인 객체와 관련된 ACL을 검색한다. 그다음 “Samantha”란 이름을 가진 principal이 해당 객체를 “관리”할 수 있게 ACE를 추가한다. 이 코드는 insertAce 메소드만 제외하면 따로 설명할 필요가 없다. insertAce 메소드의 첫 번째 인자는 새 엔트리를 추가할 Acl 상의 위치를 나타낸다. 위 예제에선 단순히 기존 ACE 목록 끝에 새 ACE를 추가하고 있다. 마지막 인자는 ACE 허가인지 거부인지를 나타내는 Boolean 값인다. 대부분은 허가이겠지만 (true), 거절이었다면 (false) 사실상 permission을 막는다.

스프링 시큐리티는 ACL 생성, 수정, 삭제 기능을 DAO나 레포지토리 연산의 일부로 자동으로 통합해주지 않는다. 따라서 각 도메인 객체마다 위와 같은 코드를 작성해야 한다. 서비스 레이어에 AOP를 적용해서, 서비스 레이어 작업에 자동으로 ACL 정보를 통합하는 걸 고려해볼만 하다. 우린 이 방법이 꽤 효과가 있었다.

위에 있는 방법을 사용해서 데이터베이스에 ACL 정보를 저장했다면, 이제 실제로 인가 결정 로직에 ACL 정보를 사용하는 일이 남았다. 여기에는 여러 가지 선택 사항이 있다. 메소드 호출 전후에 각각 호출할 AccessDecisionVoterAfterInvocationProvider를 직접 만들 수도 있다. 이 클래스는 AclService로 관련 ACL을 검색한 다음 Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)를 호출해서 권한을 부여할지 말지 결정한다. 아니면 AclEntryVoterAclEntryAfterInvocationProvider, AclEntryAfterInvocationCollectionFilteringProvider 클래스를 사용하는 방법도 있다. 이 클래스들은 모두 런타임에 ACL 정보를 평가하는 선언적인(declarative) 접근법을 제공하므로 코드를 작성하지 않아도 된다. 클래스 사용법은 샘플 어플리케이션을 참고해라.


전체 목차는 여기에 있습니다.

<< >>