[Spring Security] @Secured 기반 권한 체크

WOOK JONG KIM·2022년 12월 6일
0

패캠_java&Spring

목록 보기
87/103
post-thumbnail

MethodSecurityMetadataSource

Expression 기반의 권한 체크(@PrePost, @PostFilter ... )가 편리하고 권장되기는 하지만, 기존에 구축된 Security 들 중에 일부는@Secured기반으로 구축이 되어 있을 수 있고, 그 기반에서 소스를 유지보수 혹은 확장해 나가야 할 수도 있음

@Secured 기반의 권한 체크는 메소드의 사전에만 체크를 하기 때문에
사후 체크를 하려면 @PostAuthorize(혹은 @PostFilter) 를 사용하거나 별도의 AOP 를 설계 해야 함

@Secured 는 또한 Voter 를 추가할 수 있도록 설계되어 Voter 기반의 AccessDecisionManager와 어울림

SecurityMetadataSource는 각 인터셉터에서 관리

Method 시큐리티 메타 데이터 소스는 globalMethodSecurityConfiguration에서 관리

Metadatasource안에서 어노테이션으로 마킹해놓은 스트링(ConfigAttribute)를 주면은 이를 기반으로 권한 체크
-> Ex)PreAuthorize(“hasRole(‘ADMIN’)”)

AbstractFallback, PrePost, Delegating MethodSecurityMetadataSoruce는 Abstract MethodSecurityMetadataSource를 상속해서 구현

mapBased는 xml 기반

Delegating(중재) Metadatasoruce 메타 데이터 소스를 리스트로 가지고 있다가 앞에서 부터 차례대로 적절한 메타 데이터 소스를 골라냄
-> 만약 이를 처리할 수 있다면 만약 이를 처리할수 있다면 그 MetaDataSoruce에서 ConfigAttribute 리스트를 뽑아주는 역할

특별히 권한과 관련된 새로운 어노테이션을 추가하고 싶다면 PrePost나 Secured 처럼 MetaDataSource를 만들어서 추가를 해주어야 함


GlobalMethodSecurityConfiguration

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


protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
		return null;}


...

public MethodSecurityMetadataSource methodSecurityMetadataSource() {
  List<MethodSecurityMetadataSource> sources = new ArrayList<>();
  ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
      getExpressionHandler());
  MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
  
  // 사용자가 구현한 customMethodSecurityMetadataSoruce가 있다면 
  // 이를 Delegating metadatasource에 가장 먼저 넣어 줌
  if (customMethodSecurityMetadataSource != null) {
    sources.add(customMethodSecurityMetadataSource);
  }
  boolean hasCustom = customMethodSecurityMetadataSource != null;
  boolean isPrePostEnabled = prePostEnabled();
  boolean isSecuredEnabled = securedEnabled();
  boolean isJsr250Enabled = jsr250Enabled();
  
  // PrePost 에노테이션이 있다면 이를 메타데이터 소스에 등록
  // @EnableGlobalMethodSecurity(prePostEnabled = true)
  if (isPrePostEnabled) {
    sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
  }

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

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

코드 예시

실무에선 Pre,Post 기반 검증을 자주 사용 하지만 @Secured 동작 방식을 알고 커스터마이징도 해보자!

CustomSecurity.java

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

    String value();

}

CustomSecurityMetadataSource.java

public class CustomSecurityMetadataSource implements MethodSecurityMetadataSource {

    PrePostAnnotationSecurityMetadataSource prePostAnnotationSecurityMetadataSource;

    SecuredAnnotationSecurityMetadataSource securedAnnotationSecurityMetadataSource;


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

//        밑의 경우는 @Secured 사용한 경우
//        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);
    }

    // PrePost 메타데이터 소스 참고
    private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass, Class<A> annotationClass) {
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        A annotation = AnnotationUtils.findAnnotation(specificMethod, annotationClass);
        if (annotation != null) {
            return annotation;
        }
        return annotation;
    }
}

Custom Voter
-> 구현이후 MethodSecurityConfiguration에 추가

public class CustomVoter implements AccessDecisionVoter<MethodInvocation> {

    private final String PREFIX = "SCHOOL_";

    @Override
    public boolean supports(ConfigAttribute attribute) {
    	// ex) Attribute에 SCHOOL_PRIMARY가 담겨 오는 경우
        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(); // ROLE을 가져옴

        if(authentication.getAuthorities().stream().filter(auth -> auth.getAuthority().equals("ROLE_" + role.toUpperCase()))
                .findAny().isPresent()){
            // SCHOOL_를 ROLE_로 치환해서 그 Authority가 있으면 접근을 허용해주겠다
            return ACCESS_GRANTED;
        }
        return ACCESS_DENIED;
    }
}

controller


...

//    @Secured({"SCHOOL_PRIMARY"}) // 이를 처리해줄 Voter가 필요 (Custom Voter)
    @CustomSecurityTag("SCHOOL_PRIMARY")
    @GetMapping("/getPapersByPrimary")
    public List<Paper> getPapersByPrimary(@AuthenticationPrincipal User user){
        return paperService.getAllPapers();
    }

테스트코드

@DisplayName("5. 교장 선생님은 모든 시험지를 볼 수 있다")
    @Test
    void test_5() {
        paperService.setPaper(paper1);
        paperService.setPaper(paper2);
        paperService.setPaper(paper3);

        client = new TestRestTemplate("primary", "1111");
        ResponseEntity<List<Paper>> response = client.exchange(uri("/paper/getPapersByPrimary"),
                HttpMethod.GET, null, new ParameterizedTypeReference<List<Paper>>() {
                });

        assertEquals(200, response.getStatusCodeValue());
        assertEquals(3, response.getBody().size());
        System.out.println(response.getBody());

    }
profile
Journey for Backend Developer

0개의 댓글