본문 바로가기
Spring/Spring Security

Security AccessDecisionManager 구현 학습하기

by 코동이 2024. 5. 15.

*개요

Spring Security Authorization 구현 학습으로 좋은 설계를 확인합니다.

본 내용은 Spring Security Authorization Architecture를 기반으로 작성하였습니다.

(1) 추상골격 구현, (2) 인터페이스 합성 의존, (3) OCP 구현을 알아보겠습니다.

 

 

Voting-Based AccessDecisionManager Implementations


 투표 기반의 권한 검사는 AccessDecisionManager가 담당하며 AbstractSecurityInterceptor에 의해 호출됩니다.

 

 

 

 AccessDecisionManager 구현체의 "투표"를 활용해 사용자의 자원 접근 권한을 통제할 수 있습니다. decide()  구현이 핵심입니다. 권한 심사를 통과하지 못하는 경우 AccessDeniedException이 발생합니다.

 

public interface AccessDecisionManager {
    void decide(Authentication var1, Object var2, 
    		Collection<ConfigAttribute> var3) throws AccessDeniedException, 
            InsufficientAuthenticationException;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);
}

 

 

 

Voting Decision Manager

 

 

 AbstractAccessDecisionManagerAccessDecisionManager를 구현한 추상 클래스입니다.

 

(1) Abstractxxx로 시작하는 클래스는 보통 인터페이스를 상속하고 일부 메서드를 구현하는 추상 골격 클래스로 편하게 확장 할 수 있는 장점이 있습니다. 해당 클래스는 isAllowIfAllAbstainDecisions(), checkAllowIfAllAbstainDecisions() 등 새로운 메서드와 supports(ConfigAttribute var1), supports(Class<?> var1) 같은 구현 메서드가 있습니다. 

 

(2) 합성을 사용하여 AccessDecisionVoter를 List로 의존합니다. List 합성으로 인터페이스 AccessDecisionVoter를 의존하여 구체적인 객체를 모르더라도 다양한 구현체를 느슨하게 사용하는 장점이 있습니다. 따라서, 구현 방식이 변경되더라도 해당 클래스는 수정하지 않아도 됩니다.

 

해당 장점의 구체적인 코드는 아래에서 확인 가능합니다.

 

  • AbstractAccessDecisionManager 
더보기
public abstract class AbstractAccessDecisionManager implements AccessDecisionManager, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    private List<AccessDecisionVoter<?>> decisionVoters; //(2) 합성
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private boolean allowIfAllAbstainDecisions = false;

    protected AbstractAccessDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) {
        Assert.notEmpty(decisionVoters, "A list of AccessDecisionVoters is required");
        this.decisionVoters = decisionVoters;
    }

    public void afterPropertiesSet() {
        Assert.notEmpty(this.decisionVoters, "A list of AccessDecisionVoters is required");
        Assert.notNull(this.messages, "A message source must be set");
    }

    protected final void checkAllowIfAllAbstainDecisions() { //(1) 새로운 메서드
        if (!this.isAllowIfAllAbstainDecisions()) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }
    }

    public List<AccessDecisionVoter<?>> getDecisionVoters() {
        return this.decisionVoters;
    }

    public boolean isAllowIfAllAbstainDecisions() { //(1) 새로운 메서드
        return this.allowIfAllAbstainDecisions;
    }

    public void setAllowIfAllAbstainDecisions(boolean allowIfAllAbstainDecisions) {
        this.allowIfAllAbstainDecisions = allowIfAllAbstainDecisions;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public boolean supports(ConfigAttribute attribute) { //(1) 구현 메서드
        Iterator var2 = this.decisionVoters.iterator();

        AccessDecisionVoter voter;
        do {
            if (!var2.hasNext()) {
                return false;
            }

            voter = (AccessDecisionVoter)var2.next();
        } while(!voter.supports(attribute));

        return true;
    }

    public boolean supports(Class<?> clazz) { //(1) 구현 메서드
        Iterator var2 = this.decisionVoters.iterator();

        AccessDecisionVoter voter;
        do {
            if (!var2.hasNext()) {
                return true;
            }

            voter = (AccessDecisionVoter)var2.next();
        } while(voter.supports(clazz));

        return false;
    }
}

 

  (3) AbstractAccessDecisionManagerAffirmativeBased(1명만이라도), ConsensusBased(다수결), UnanimouseBased(만장일치) 3개 정책이 있습니다. 이미 AbstractAccessDecisionManager가 대부분의 메서드를 구현하였으므로 정책을 추가할때마다 decide()만 재정의하면 됩니다. 또한 정책을 제거할 때도 다른 코드를 수정하지 않아도 정책 클래스만 제거하면 됩니다. OCP를 만족합니다.

 

  • AffirmativeBased (1명만이라도)
더보기
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    int deny = 0;
    Iterator var5 = this.getDecisionVoters().iterator();

    while(var5.hasNext()) {
        AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
        int result = voter.vote(authentication, object, configAttributes);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Voter: " + voter + ", returned: " + result);
        }

        switch (result) {
            case -1:
                ++deny;
                break;
            case 1:
                return;
        }
    }

    if (deny > 0) {
        throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    } else {
        this.checkAllowIfAllAbstainDecisions();
    }
}

 

  • ConsensusBased(다수결)
더보기
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    int grant = 0;
    int deny = 0;
    Iterator var6 = this.getDecisionVoters().iterator();

    while(var6.hasNext()) {
        AccessDecisionVoter voter = (AccessDecisionVoter)var6.next();
        int result = voter.vote(authentication, object, configAttributes);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Voter: " + voter + ", returned: " + result);
        }

        switch (result) {
            case -1:
                ++deny;
                break;
            case 1:
                ++grant;
        }
    }

    if (grant <= deny) {
        if (deny > grant) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else if (grant == deny && grant != 0) {
            if (!this.allowIfEqualGrantedDeniedDecisions) {
                throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
            }
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }
}

 

  • UnanimousBased(만장일치)
더보기
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) throws AccessDeniedException {
    int grant = 0;
    List<ConfigAttribute> singleAttributeList = new ArrayList(1);
    singleAttributeList.add((Object)null);
    Iterator var6 = attributes.iterator();

    while(var6.hasNext()) {
        ConfigAttribute attribute = (ConfigAttribute)var6.next();
        singleAttributeList.set(0, attribute);
        Iterator var8 = this.getDecisionVoters().iterator();

        while(var8.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var8.next();
            int result = voter.vote(authentication, object, singleAttributeList);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Voter: " + voter + ", returned: " + result);
            }

            switch (result) {
                case -1:
                    throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
                case 1:
                    ++grant;
            }
        }
    }

    if (grant <= 0) {
        this.checkAllowIfAllAbstainDecisions();
    }
}

 

 

 AccessDecisionVoter는 "심의자" 입니다. 인증이 완료된 사용자가 자원에 접근할 때 권한을 심사합니다. 심의자 투표는 구현체의 검증 기준에 따라 ACCESS_GRANTED(승인, 1), ACCESS_DENIED(거절, -1), ACCESS_ABSTAIN(보류, 0) 중 1개를 반환합니다.

 

int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3)의 매개변수 3가지는 다음과 같습니다.

  • Authentication - 인증 정보(user)
  • FilterInvocation - 요청정보(.antMatcher("/user"))
  • ConfigAttributes - 권한정보(.permitAll(), .hasRole("USER"))

 

public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);

    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

 

 심의자는 RoleVoterAuthenticatedVoter가 있습니다. RoleVoter는 이름을 확인하는 간단한 방식으로 매개변수로 전달된 ConfigAttributeROLE_로 시작하는 권한을 확인해 투표합니다. AuthenticatedVoteranonymous, fully-authenticated, remember-me 3개를 구분하는데 인증 종류가 어떤 것인지 확인해 투표합니다. 개발자는 원하는 심의자를 빈으로 등록하면 되고, custom하게 만들 수도 있습니다. (default는 RoleVoter)

 

 

  • RoleVoter
더보기
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if (authentication == null) {
            return -1;
        } else {
            int result = 0;
            Collection<? extends GrantedAuthority> authorities = this.extractAuthorities(authentication);
            Iterator var6 = attributes.iterator();

            while(true) {
                ConfigAttribute attribute;
                do {
                    if (!var6.hasNext()) {
                        return result;
                    }

                    attribute = (ConfigAttribute)var6.next();
                } while(!this.supports(attribute));

                result = -1;
                Iterator var8 = authorities.iterator();

                while(var8.hasNext()) {
                    GrantedAuthority authority = (GrantedAuthority)var8.next();
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return 1;
                    }
                }
            }
        }
    }

 

 

  • AuthenticatedVoter
더보기
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
    int result = 0;
    Iterator var5 = attributes.iterator();

    ConfigAttribute attribute;
    do {
        do {
            if (!var5.hasNext()) {
                return result;
            }

            attribute = (ConfigAttribute)var5.next();
        } while(!this.supports(attribute));

        result = -1;
        if ("IS_AUTHENTICATED_FULLY".equals(attribute.getAttribute()) && this.isFullyAuthenticated(authentication)) {
            return 1;
        }

        if ("IS_AUTHENTICATED_REMEMBERED".equals(attribute.getAttribute()) && (this.authenticationTrustResolver.isRememberMe(authentication) || this.isFullyAuthenticated(authentication))) {
            return 1;
        }
    } while(!"IS_AUTHENTICATED_ANONYMOUSLY".equals(attribute.getAttribute()) || !this.authenticationTrustResolver.isAnonymous(authentication) && !this.isFullyAuthenticated(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication));

    return 1;
}

 

 

반응형

'Spring > Spring Security' 카테고리의 다른 글

Authentication 아키텍쳐  (0) 2024.05.21
Spring Security 구조 메커니즘  (0) 2024.05.17
AuthToken 사용하기  (0) 2021.10.26
JWT 토큰  (0) 2021.10.26
임시권한 부여하기  (0) 2021.10.23