본문 바로가기

Spring 정리/Spring Security

@Secured 기반 권한체크

728x90
반응형

 기존에는 Expression 기반의 어노테이션을 이용한 권한체크를 하였다. 기존에는 시큐리티가 @Secured로 구축된 것이 많다. 이 권한 체크들도 내부를 분석해볼만한 가치가 있다. CustomVoter도 Secured 기반으로 만들어본다. 어노테이션을 커스텀하게 만드는 것도 추가한다.

 

 

 

 

SecurityInterceptor는 filter와 method 2곳에 위치하는데, AbstractSecurityInteceptor에 공통코드가 들어가있다. Filter에서 필요한 권한 검사의 내용은 MetadataSource에 있다. (Method에 필요한 것도 Method만의 MetadataSource에 있다.) MetadataSource안에 에노테이션으로 권한을 마킹한 정보들을 configAttribute라고 한다. 이것을 확인해서 권한 검사를 한다.

 

FilterInvocation은 Expression기반이다. 따라서 MetadataSource가 확장가능성이 없다.

 

MethodSecurityMetadataSource를 분석한다.

 

 

MethodSerucirtyMetadataSource는 여러 개의 MetadataSource를 가지고 있다. 

 

MapBased는 xml에서 PointCut과 같이 ROLE과 연동하여 작동하는데, xml이므로 잘 사용하지 않는다. 

나머지 Secured, PrePost, Delegating이 남는다.

 

Secured의 경우 @Secured를 사용하여 configAttribute를 구성한다.

PrePost@PreAuthorize, @PreFilter, @PostAuthorize, @PostFilter를 사용하여 MetadataSource를 구성한다.

Delegating은 MetadataSource를 가지고 있다가 적절한 MetadataSource를 골라서 configAttribute를 뽑아준다.

 

* 권한에서 사용하는 것은 Secured, PrePost 중 하나인데, 나만의 어노테이션을 추가하고 싶다면?

 

메소드 권한을 설정하는 GlobalMethodSecurityConfiguration 에서 methodSecurityMetadataSource() 를 만들어 내는 곳에서 어노테이션을 파싱해 MethodSecurityMetadataSource 를 만들어 낸다.

 

GlobalMethodSecurityConfiguration클래스의 customMethodSecurityMetadataSource를 재정의하면 원하는 에노테이션 권한 검사를 커스터마이징할 수 있다.

 

protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { return null; }

 

 

public MethodSecurityMetadataSource methodSecurityMetadataSource() {
  List<MethodSecurityMetadataSource> sources = new ArrayList<>();
  ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
      getExpressionHandler());
  MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
  if (customMethodSecurityMetadataSource != null) {
    sources.add(customMethodSecurityMetadataSource);
  }
  boolean hasCustom = customMethodSecurityMetadataSource != null;
  boolean isPrePostEnabled = prePostEnabled();
  boolean isSecuredEnabled = securedEnabled();
  boolean isJsr250Enabled = jsr250Enabled();

  if (isPrePostEnabled) {
    sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
  }

  if (isSecuredEnabled) {
    sources.add(new SecuredAnnotationSecurityMetadataSource());
  }

  if (isJsr250Enabled) {
    ...
  }
  return new DelegatingMethodSecurityMetadataSource(sources);
}

 

Delegating으로 MetadataSource를 리스트로 가지게 된다.

 

isPrePostEnabled, isSecuredEnabled에 커스터마이징한 객체를 추가하면 sources.add()에 추가하면 MetadataSource에 추가된다.

 

 

AbstractSecurityInterceptor 클래스에서 다음을 debug하여 metadataSource를 조회해 ConfigAttribute를 살펴본다.

 

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);

 

Filter도 작동하고, Method도 작동하는 공통 클래스이기 떄문에 먼저 Filter가 실행되므로 우리는 Method를 검사한다.

 

 

prePostAnnotationSecurityMetadataSource가 있음을 알 수 있다.

 

 

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

 

만약에 prePostEnabled 뿐만 아니라, securedEnabled을 추가한다면?

 

 

SecuredAnnotationSecurityMetadatSource도 추가된 것을 알 수 있다. 이제 @Secured 사용해서 권한 검사를 진행할 수 있다. 

 

@PrePost, @Secured 2개를 모두 사용한다면, ConfigAttribute가 각각 다르게 만들어진다. 따라서 한쪽에서 존재가 확인되면, 하나만 적용을 한다. 따라서, @PrePost, @Secured를 동시에 사용하면 먼저 @PrePost가 감지하기 때문에 @Secured는 무용지물이 된다.

 

 

 MethodSecurityMeatadatSource를 구현하는 클래스를 만들면, MetadataSource에 권한 검사를 추가할 수 있다.

코드로 적용과정을 알아본다. 나만의 @Secured 설정을 만든다.

 

    @Secured("SCHOOL_PRIMARY")
    @GetMapping("/getPapersByPrimary")
    public List<Paper> getPaperByPrimary(@AuthenticationPrincipal User user) {
        return paperService.getAllPapers();
    }

 

, 그런데 "SCHOOL_PRIMARY"는 정의하지 않았기 때문에 전혀 검사가 되지 않는다. voter를 추가해야 한다. SCHOOL_로 시작하는 권한을 검색하고 비교하여 ACCESS 혹은 DENIED를 리턴한다.

 

public class CustomVoter implements AccessDecisionVoter<MethodInvocation> {
    private final String PREFIX = "SCHOOL_";

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute().startsWith(PREFIX);
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return MethodInvocation.class.isAssignableFrom(clazz);
    }

    @Override
    public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {
        String role = attributes.stream().filter(attr -> attr.getAttribute().startsWith(PREFIX))
                .map(attr->attr.getAttribute().substring(PREFIX.length()))
                .findFirst().get();
        
        if(authentication.getAuthorities().stream()
                .anyMatch(auth->auth.getAuthority().equals("ROLE_" + role.toUpperCase()))) {
            return ACCESS_GRANTED;
        }
        return ACCESS_DENIED;
    }
}

 

실제 확인하기 위해서, 교장선생님으로 getPapersByPrimary() 실행 테스트를 하는데, AbstarctSecurityInterceptor 클래스에서 beforeInvocation 메서드의 다음 부분을 디버그한다.

 

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);

 

얻어온 attributes에는 "SCHOOL_PRIMARY"가 있다. 새로운 voter가 작동한 것이다.

 

 

투표하는 decide() 메서드에서 4개의 voter들이 동작하는데, 우리가 만든 CustomVoter()에서 통과가 된다.

 

decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new CustomVoter());

 

마지막 4번쨰 CustomVoter

 

 

MetadataSource에 추가하기 위한 CustomMetadatSource를 만든다.

 

 

public class CustomMetadataSource implements MethodSecurityMetadataSource {
	@Override
	public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
		if(method.getName().equals("getPapersByPrimary") && targetClass == PaperController.class) {
			return List.of(new SecurityConfig("SCHOOL_PRIMARY"));
		}
		return null;
	}

	@Override
	public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
		return null;
	}

	@Override
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		return null;
	}

	@Override
	public boolean supports(Class<?> clazz) {
		return MethodInvocation.class.isAssignableFrom(clazz);
	}
}

 

@Secured("SCHOOL_PRIMARY")가 있지 않더라도 알아서 getPaperByPrimary() 메서드에 검사하도록 voter를 커스터마이징하였다.

 

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
...
    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new CustomMetadataSource();
    }
...

 

CustomMetadataSource가 검사에서 자동으로 작동할 수 있도록 MethodSecurityMetadataSource를 등록해준다.

 

 

 

 적용이 되고 나면 dataMetadataSource()에 커스터마이징한 CustomMetadataSource가 첫번째로 전달되어 검사를 하게된다.

 

 

attributes에는 SCHOOL_PRIMARY가 정상적으로 들어간다.

 

 

SecuredAnnotationSecurityMetadataSource 클래스의 findAttributes 메서드를 확인한다.

 

@Override
protected Collection<ConfigAttribute> findAttributes(Method method, Class<?> targetClass) {
	return processAnnotation(AnnotationUtils.findAnnotation(method, this.annotationType));
}

 

public class CustomMetadataSource implements MethodSecurityMetadataSource {
	@Override
	public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
		CustomSecurityTag annotation = findAnnotation(method, targetClass, CustomSecurityTag.class);

		if(annotation != null) {
			return List.of(new SecurityConfig(annotation.value()));
		}
		return null;
	}

...

private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass, Class<A> annotationClass) {
  Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
  return AnnotationUtils.findAnnotation(specificMethod, annotationClass);
}
...

 

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomSecurityTag {
	String value();
}

 

//    @Secured("SCHOOL_PRIMARY")
@CustomSecurityTag("SCHOOL_PRIMARY")
@GetMapping("/getPapersByPrimary")
public List<Paper> getPaperByPrimary(@AuthenticationPrincipal User user) {
  return paperService.getAllPapers();
}

 

 

Secured 동작방식 공부하기에는 좋은 커스터마이징이지만, 이런 권한체크가 post를 지원하지 않고 prehead에서만 지원하므로 실제 운영에서 사용이 유용하지는 않다. 대부분의 권한체크는 애노테이션 기반보다는 pre/post로 expression으로 검사하는게 더 유용하다. 

 

SecurityInterceptorAccessDecisionManager로 메서드 진입 이전에 권한측정, AfterInvocationManager으로 메서드 실행 이후 권한측정을 한다. 그 이외에 RunAsManager가 있으며, 중요한 기능은 아니지만 변칙적인 임시 권한을 부여해서 바로 뺏는 기능을 구현할 수 있다.

728x90
반응형

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

JWT 토큰  (0) 2021.10.26
임시권한 부여하기  (0) 2021.10.23
메서드 후처리  (0) 2021.10.19
권한위원회 voter  (0) 2021.10.17
스프링 시큐리티의 권한(Authorization)  (0) 2021.10.15