모르지 않다는 것은 아는것과 다르다.

Spring Security

Method 시큐리티 프로세스 커스텀

채마스 2022. 2. 28. 21:00

개요

  • Method 레벨에서 시큐리티를 적용시키고 싶다.
  • Method 시큐리티는 AOP 기반으로 구현되어있다.





Method 시큐리티와 AOP

  • Method 시큐리티에서 AOP 적용 원리는 아래와 같다.

  • ProxyFactory 에서 프록시 객체를 생성한다.
  • 프록시 객체는 advisor 를 통해서 advice 를 실행시킨다.
  • Pointcut 으로 설정된 부분에서 인가를 처리한다.
  • 인가처리가 끝난 후 프록시 객체가 아닌 실제 클래스의 메소드를 실행한다.





Method 에 애노테이션 방식의 권한 설정

  • 보안이 필요한 메소드에 설정한다.
  • @PreAuthorize, @PostAuthorize, @Secured, @RolesAllowed 등이 있다.
  • @PreAuthorize, @PostAuthorize
    • SpEL 지원
    • PrePostAnnotationSecurityMetadataSource 가 담당한다.
    • ex> @PreAuthorize("hasRole('ROLE_USER’) and (#account.username == principal.username)")
    • @EnableGlobalMethodSecurity(prePostEnabled = true) 를 설정해 줘야한다.
  • @Secured, @RolesAllowed
    • SpEL 미지원
    • SecuredAnnotationSecurityMetadataSource, Jsr250MethodSecurityMetadataSource 가 담당한다.
    • ex> @Secured ("ROLE_USER"), @RolesAllowed("ROLE_USER")
    • @EnableGlobalMethodSecurity(securedEnabled = true) 를 설정해 줘야한다.





초기화 과정

  • DefaultAdvisorAutoProxyCreator 는 빈을 검사하면서 보안(@Secured)이 걸려있는 메소드를 찾아서, 그 메소드를 가지고 있는 빈의 프록시 객체를 생성한다.
  • 프록시 객체를 생성하는 과정은 아래와 같다.
  • 먼저 DefaultAdvisorAutoProxyCreator 는 MethodSecurityMetadataSourceAdvisor 를 통해서 빈을 검사한다.
  • MethodSecurityMetadataSourceAdvisor 는 MethodSecurityMetadataSourcePointcut 와 MethodSecurityInterceptor 를 가지고 있다.
  • MethodSecurityMetadataSourcePointcut 는 MethodSecurityMetadataSource 를 사용해서 메소드에 보안이 설정되어 있는지 탐색한다.
  • 정보를 전달받은 MethodSecurityMetadataSource 는 class 정보와 method 정보를 파싱해서 보안 메소드가 설정된 빈일 경우 프록시 생성 대상으로 간주하고 DefaultAdvisorAutoProxyCreator 는 프록시 객체를 생성한다.
  • 또한 MethodMap 에 Key 는 method 명, value 는 권한으로 저장된다.
  • MethodeSecurityInterceptor 가 Advice 이다.
  • 그렇기 때문에 보안 메소드일 경우에 MethodeSecurityInterceptor 에 어드바이스가 등록된다.
  • MethodeSecurityInterceptor 가 보안이 걸려있는 order() 메소드 전후로 부가기능(인가처리)을 처리하는 역할을 한다.





실행과정

  • 먼저 사용자가 order(), display() 메소드를 호출했다고 해보자.
  • order 의 경우 @Secured 가 걸려있고 display 의 경우 걸려있지 않다.
  • 그렇기 때문에 display 는 advice 가 등록되지 않을 것이고, 보안이 적용되지 않을 것이다.
  • order 의 경우 MethodSecurityInterceptor 에 advice 가 등록되어 있을 것이고, MethodSecurityInterceptor 는 인가를 처리한다.
  • 만약 인가 처리가 승인된다면 프록시가 아닌 실제 OrderService 의 order 메소드가 실행된다.
  • 만약 인가 처리가 승인되지 않았다면 AccessDeniedException 이 발생한다.






Filter 기반 Url 방식, AOP 기반 Method 방식 차이

  • 두 방식은 사용되는 기술과 클래스가 다를 뿐이다.
  • 처리되는 방식도 비슷하다.
  • 먼저 url 의 경우 FilterSecurityInterceptor 라는 필터가 인가를 처리한다.
  • method 의 경우 MethodSecurityInterceptor 라는 Advice 가 인가를 처리한다.
  • FilterSecurityInterceptor, MethodSecurityInterceptor 모두 AccessDecisionManager 에게 권한정보를 넘겨주는 것이 목적이다.
  • 서버가 구동될때 초기화되는데, url 방식의 경우 RequestMap 에 권한 정보가 저장되고, method 방식의 경우, MethodMap 에 권한 정보가 저장된다.
  • FilterSecurityInterceptor 는 FilterInvocationSecurityMetadataSource 에게 권한 정보를 요청하고, MethodSecurityInterceptor 의 경우엔 MethodSecurityMetadataSource 에게 권한 정보를 요청해서 반환 받는다.
  • 각각 반환 받은 권한 정보를 AccessDecisionManager 에게 전달한다.





Map 기반 DB 연동

  • 애노테이션 설정 방식이 아닌 맵 기반으로 권한을 설정하는 방식이다. -> 하지만 애노테이션 방식과 마찬가지로 AOP 기반으로 처리된다.
  • 기본적인 구현이 완성되어 있다.
  • DB 로부터 자원과 권한정보를 매핑한 데이터를 전달하면 메소드 방식의 인가처리가 이루어 진다.

  • 먼저 사용자가 admin 요청을 보낸다. admin 메소드는 ROLE_ADMIN 권한이 필요하다.
  • MethodSecurityInterceptor 는 MapBasedMethodSecurityMetadataSource 에게 권한 정보를 요청한다.
  • MapBasedMethodSecurityMetadataSource 는 MethodMap 를 가지고 있고, MethodMap 에는 위와 같이 권한정보들이 저장되어 있다.

  • MapBasedMethodSecurityMetadataSource 에 methodMap 을 넣어줘야 한다.
  • DB 에서 권한 정보를 가져와 [패키지 + 클래스 + 메소드 | 권한] 으로 매핑해서 methodMap 을 구성한다.

  • 애노테이션 방식과 처리과정이 같다.
  • 사용자가 order() 를 요청한다면 order() 를 가지고있는 빈의 프록시 객체가 MethodSecurityInterceptor 에 등록된 advice 로 인가처리를 진행한다.





코드 구현

  • MethodResourcesMapFactoryBean 구현
    @Slf4j
    public class MethodResourcesMapFactoryBean implements FactoryBean<LinkedHashMap<String, List<ConfigAttribute>>> {

        private SecurityResourceService securityResourceService;
        private String resourceType;

        public void setResourceType(String resourceType) {
            this.resourceType = resourceType;
        }

        public void setSecurityResourceService(SecurityResourceService securityResourceService) {
            this.securityResourceService = securityResourceService;
        }

        private LinkedHashMap<String, List<ConfigAttribute>> resourcesMap;

        public void init() {
            if ("method".equals(resourceType)) {
                resourcesMap = securityResourceService.getMethodResourceList();
            }else if("pointcut".equals(resourceType)){
                resourcesMap = securityResourceService.getPointcutResourceList();
            }
        }

        public LinkedHashMap<String, List<ConfigAttribute>> getObject() {
            if (resourcesMap == null) {
                init();
            }
            return resourcesMap;
        }

        @SuppressWarnings("rawtypes")
        public Class<LinkedHashMap> getObjectType() {
            return LinkedHashMap.class;
        }

        public boolean isSingleton() {
            return true;
        }
    }
  • MethodResourcesMapFactoryBean 은 DB로 부터 얻은 권한/자원 정보를 ResourceMap 을 빈으로 생성해서 MapBasedMethodSecurityMetadataSource 에 전달한다.
public LinkedHashMap<String, List<ConfigAttribute>> getMethodResourceList() {
    LinkedHashMap<String, List<ConfigAttribute>> result = new LinkedHashMap<>();
    List<Resources> resourcesList = resourcesRepository.findAllMethodResources();
    resourcesList.forEach(re ->
            {
                List<ConfigAttribute> configAttributeList = new ArrayList<>();
                re.getRoleSet().forEach(ro -> {
                    configAttributeList.add(new SecurityConfig(ro.getRoleName()));
                });
                result.put(re.getResourceName(), configAttributeList);
            }
    );
    return result;
    }
  • MapBaseMethodSecurityMetadataSource 에게 넘겨줄 resourceMap 을 만드는 과정이다.
  • 메소드 계층에서의 보안을 위한 MethodSecurityConfig 파일 생성한다.
@Configuration
@EnableGlobalMethodSecurity
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Autowired
    private SecurityResourceService securityResourceService;

    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return mapBasedMethodSecurityMetadataSource();
    }

    @Bean
    public MapBasedMethodSecurityMetadataSource mapBasedMethodSecurityMetadataSource() {
        return new MapBasedMethodSecurityMetadataSource(methodResourcesMapFactoryBean().getObject());
    }

    @Bean
    public MethodResourcesMapFactoryBean methodResourcesMapFactoryBean(){
        MethodResourcesMapFactoryBean methodResourcesMapFactoryBean = new MethodResourcesMapFactoryBean();
        methodResourcesMapFactoryBean.setSecurityResourceService(securityResourceService);
        methodResourcesMapFactoryBean.setResourceType("method");
        return methodResourcesMapFactoryBean;
    }
}
  • 위에서 구현한 MethodResourcesMapFactoryBean 을 빈으로 등록한다. -> 그러기 위해서는 securityResourceService 를 MethodResourcesMapFactoryBean 에 등록해 줘야한다.
  • methodResourcesMapFactoryBean().getObject() 를 통해서 resoureMap 을 MapBasedMethodSecurityMetadataSource 에게 전달한다.
  • MapBasedMethodSecurityMetadataSource 을 빈으로 등록하고 재정의한 customMethodSecurityMetadataSource 메소드의 반환값으로 넘겨준다.





ProtectPointcutPostProcessor

  • 메소드 방식의 인가처리를 위한 자원 및 권한정보 설정 시 자원에 포인트 컷 표현식을 사용할 수 있도록 지원하는 클래스다.

  • DB 로 부터 Pointcut 표현식과 권한 정보를 가져와서 ResourceMap 을 생성한다.
  • ResourceMap 을 ProtextPointcutPostProcessor 에 담겨있는 PointcutMap 에 전달한다.
  • ProtextPointcutPostProcessor 는 PointcutMap 에 담겨있는 정보를 바탕으로 찾은 빈의 TargetClass, Method, ConfigAttribute 정보를 추출해서 MapBaseMethodSecurityMetadataSource 에게 전달한다.
  • 그 외에는 Map 기반의 DB 연동 방식과 같다.
  • MethodResourcesMapFactoryBean 은 DB로 부터 얻은 권한/자원 정보를 ResourceMap 빈으로 생성해서 ProtectPointcutPostProcessor 에 전달한다.






코드 구현

    @Bean
    BeanPostProcessor protectPointcutPostProcessor() throws Exception {

        Class<?> clazz = Class.forName("org.springframework.security.config.method.ProtectPointcutPostProcessor");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(MapBasedMethodSecurityMetadataSource.class);
        declaredConstructor.setAccessible(true);
        Object instance = declaredConstructor.newInstance(mapBasedMethodSecurityMetadataSource());
        Method setPointcutMap = instance.getClass().getMethod("setPointcutMap", Map.class);
        setPointcutMap.setAccessible(true);
        setPointcutMap.invoke(instance, pointcutResourcesMapFactoryBean().getObject());

        return (BeanPostProcessor)instance;
    }
  • Config 파일에서 빈으로 등록할 때 접근제어자가 디폴트이기 때문에 위와 같이 리플렉션으로 객체를 생성해야한다.




REFERENCES

  • 정수원님의 스프링 시큐리티

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

JWT  (0) 2022.02.28
ProxyFactory 를 이용한 동적 Method 인가 처리  (0) 2022.02.28
URL 시큐리티 프로세스 커스텀  (0) 2022.02.28
권한 계층 적용하기  (0) 2022.02.28
Voter 커스텀 하기  (0) 2022.02.28