FilterSecurityInterceptor

midas·2022년 5월 21일
0

🎬 FilterSecurityInterceptor란?

  • 필터 체인상에서 가장 마지막에 위치
  • 사용자가 가지고 있는 권한과 리소스에서 요구하는 권한을 취합하여 접근을 허용할건지 결정!

해당 필터가 호출되는 시점에서 사용자는 이미 인증이 완료된 상태입니다.
(익명 사용자도 인증이 완료된 것으로 ROLE_ANONYMOUS 권한)

보호되는 리소스에서 요구하는 권한 정보는 SecurityMetadataSource 인터페이스를 통해 ConfigAttribute 타입으로 가져오게 됩니다.

흐름은?

  1. FilterSecurityInterceptor
  2. AbstractSecurityInterceptor
  3. AccessDecisionManager
  4. AccessDecisionVoter

코드 분석

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
  if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
    filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
  } else {
    if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
      filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
    }

    // ✨ AbstractSecurityInterceptor → beforeInvocation()를 실행하게 됩니다.
    InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

    try {
      filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
    } finally {
      super.finallyInvocation(token);
    }

    super.afterInvocation(token, (Object)null);
  }
}

AbstractSecurityInterceptor

protected InterceptorStatusToken beforeInvocation(Object object) {
  Assert.notNull(object, "Object was null");
  if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
    throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
  } else {

    // ✨ 이 부분이 리소스에서 요구하는 권한 목록들을 가져오는 구간입니다.
    //    SecurityMetadataSource 인터페이스 구현체가 ConfigAttribute 타입으로 가져오게 됩니다.
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
    if (CollectionUtils.isEmpty(attributes)) {
      Assert.isTrue(!this.rejectPublicInvocations, () -> {
        return "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'";
      });
      if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Authorized public object %s", object));
      }

      this.publishEvent(new PublicInvocationEvent(object));
      return null;
    } else {

      // ✨ 리소스에서 요구하는 권한이 있는 경우에 이제 해당 권한을 가지고 있는지 확인해야 겠죠?

      if (SecurityContextHolder.getContext().getAuthentication() == null) {
        this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
      }

      Authentication authenticated = this.authenticateIfRequired();
      if (this.logger.isTraceEnabled()) {
          this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
      }

      // ✨ 여기서 결국 처리를 하게 됩니다!
      this.attemptAuthorization(object, attributes, authenticated);
      if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
      }

      if (this.publishAuthorizationSuccess) {
        this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
      }

      Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
      if (runAs != null) {
        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
        newCtx.setAuthentication(runAs);
        SecurityContextHolder.setContext(newCtx);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
        }

        return new InterceptorStatusToken(origCtx, true, attributes, object);
      } else {
          this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
          return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
      }
    }
  }
}

// ✨ 권한을 가졌는지 체크하기!
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
  try {
    // ✨ AccessDecisionManager의 구현체인 AffirmativeBased가 실행되게 됩니다.
    this.accessDecisionManager.decide(authenticated, object, attributes);
  } catch (AccessDeniedException var5) {
    if (this.logger.isTraceEnabled()) {
      this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager));
    } else if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
    }

    this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5));
    throw var5;
  }
}

AccessDecisionManager

AccessDecisionVoter 목록을 가지고, 투표를 하게 되어 결과를 취합해서 접근 승인 여부를 결정하게 됩니다.

여기에는 총 3가지 구현체를 제공하게 됩니다.
먼저 기본값인 AffirmativeBased부터 살펴보겠습니다.

AffirmativeBased (기본값)

이 친구는 승인 되는 녀석이 하나라도 있다면 접근이 승인됩니다!

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);
    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

이 친구는 다수의 Voter가 승인을 해야 접근이 승인됩니다!

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);
    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);
      switch (result) {
        case -1: // ✨ 한명이라도 반대를 하게 되면 Exception을 던지는것을 볼 수 있죠?
          throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        case 1:
          ++grant;
      }
    }
  }

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

AccessDecisionVoter

각각의 Voter는 접근을 승인, 거절, 보류를 판단하게 됩니다.

int ACCESS_GRANTED = 1; // 승인
int ACCESS_ABSTAIN = 0; // 보류
int ACCESS_DENIED = -1; // 거절

int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);

WebExpressionVoter

SpEL(Spring Expression Language)

  • DefaultWebSecurityExpressionHandler, WebSecurityExpressionRoot 구현에 의존함
  • DefaultWebSecurityExpressionHandler.createSecurityExpressionRoot() 메서드에서 WebSecurityExpressionRoot 객체를 생성함
  • WebSecurityExpressionRoot 제공 메서드
    • hasRole(String role)
    • ...

Custom Voter

....

참고

profile
BackEnd 개발 일기

0개의 댓글