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

Spring Security

ProxyFactory 를 이용한 동적 Method 인가 처리

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

개요

  • 메소드에 실시간으로 보안처리를 하고 싶다.
  • url 인가 처리의 경우 필터기반이기 때문에, 매 요청시 인가처리를 할 수 있지만, 메소드 인가 처리는 AOP 기반이기 때문에, 애플리케이션이 구동되는 시점, 즉 초기화 시점에서 프록시 객체와 Advice 가 등록이 되어있어야 한다.
  • 그렇기 때문에 메소드 인가 처리를 동적으로 처리하기 위해서는 실시간으로 프록시 객체를 생성하고 Advice 를 등록하는 로직이 필요하다.

AOP 와 ProxyFactory

코드 구현

@Component
public class MethodSecurityService {

    private MapBasedMethodSecurityMetadataSource mapBasedMethodSecurityMetadataSource;
    private AnnotationConfigServletWebServerApplicationContext applicationContext;
    private CustomMethodSecurityInterceptor methodSecurityInterceptor;

    private Map<String, Object> proxyMap = new HashMap<>();
    private Map<String, ProxyFactory> advisedMap = new HashMap<>();
    private Map<String, Object> targetMap = new HashMap<>();

    public MethodSecurityService(MapBasedMethodSecurityMetadataSource mapBasedMethodSecurityMetadataSource, AnnotationConfigServletWebServerApplicationContext applicationContext, CustomMethodSecurityInterceptor methodSecurityInterceptor) {
        this.mapBasedMethodSecurityMetadataSource = mapBasedMethodSecurityMetadataSource;
        this.applicationContext = applicationContext;
        this.methodSecurityInterceptor = methodSecurityInterceptor;
    }

    public void addMethodSecured(String className, String roleName) throws Exception{

        int lastDotIndex = className.lastIndexOf(".");
        String methodName = className.substring(lastDotIndex + 1);
        String typeName = className.substring(0, lastDotIndex);
        Class<?> type = ClassUtils.resolveClassName(typeName, ClassUtils.getDefaultClassLoader());
        String beanName = type.getSimpleName().substring(0, 1).toLowerCase() + type.getSimpleName().substring(1);

        ProxyFactory proxyFactory = advisedMap.get(beanName);
        Object target = targetMap.get(beanName);

        if(proxyFactory == null){

            proxyFactory = new ProxyFactory();
            if(target == null) {
                proxyFactory.setTarget(type.getDeclaredConstructor().newInstance());

            }else{
                proxyFactory.setTarget(target);
            }
            proxyFactory.addAdvice(methodSecurityInterceptor);

            advisedMap.put(beanName, proxyFactory);

        }else{

            int adviceIndex = proxyFactory.indexOf(methodSecurityInterceptor);
            if(adviceIndex == -1){
                proxyFactory.addAdvice(methodSecurityInterceptor);
            }
        }

        Object proxy = proxyMap.get(beanName);

        if(proxy == null){

            proxy = proxyFactory.getProxy();
            proxyMap.put(beanName, proxy);

            List<ConfigAttribute> attr = Arrays.asList(new SecurityConfig(roleName));
            mapBasedMethodSecurityMetadataSource.addSecureMethod(type,methodName, attr);

            DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)applicationContext.getBeanFactory();
            registry.destroySingleton(beanName);
            registry.registerSingleton(beanName, proxy);
        }
    }

    public void removeMethodSecured(String className) throws Exception{

        int lastDotIndex = className.lastIndexOf(".");
        String typeName = className.substring(0, lastDotIndex);
        Class<?> type = ClassUtils.resolveClassName(typeName, ClassUtils.getDefaultClassLoader());
        String beanName = type.getSimpleName().substring(0, 1).toLowerCase() + type.getSimpleName().substring(1);
        Object newInstance = type.getDeclaredConstructor().newInstance();

        DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)applicationContext.getBeanFactory();

        ProxyFactory proxyFactory = advisedMap.get(beanName);

        if(proxyFactory != null){
            proxyFactory.removeAdvice(methodSecurityInterceptor);

        }else{
            registry.destroySingleton(beanName);
            registry.registerSingleton(beanName, newInstance);
            targetMap.put(beanName,newInstance);
        }
    }
}
  • 메소드 보안 최초 설정 시 대상 빈의 프록시 객체 생성하고 메소드에 Advice 등록하여 AOP 를 적용시킨다.
  • MapBasedMethodSecurityMetadataSource 에 자원 및 권한 정보 전달한다.
  • DefaultSingletonBeanRegistry 로 실제 빈을 삭제하고 프록시 객체를 빈 참조로 등록한다.
  • 보안이 적용된 메소드 호출 시 Advice 가 작동한다.
  • 메소드 보안 해제 시 메소드에 등록된 Advice 를 제거한다.
  • 메소드 보안 재 설정 시 메소드에 등록된 Advice 를 다시 등록한다.





CustomMethodSecurityInterceptor 구현

public class CustomMethodSecurityInterceptor extends AbstractSecurityInterceptor implements
        MethodInterceptor {
    private MethodSecurityMetadataSource securityMetadataSource;

    public Class<?> getSecureObjectClass() {
        return MethodInvocation.class;
    }

    public Object invoke(MethodInvocation mi) throws Throwable {
        InterceptorStatusToken token = super.beforeInvocation(mi);

        Object result;
        try {
            result = mi.proceed();
        }
        finally {
            super.finallyInvocation(token);
        }
        return super.afterInvocation(token, result);
    }

    public MethodSecurityMetadataSource getSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void setSecurityMetadataSource(MethodSecurityMetadataSource newSource) {
        this.securityMetadataSource = newSource;
    }
}
  • advice 처리를 하던 MethodSecurityInterceptor 를 커스텀하기 위해서 CustomMethodSecurityInterceptor 를 만들었다.
  • 그 이유는 MethodSecurityInterceptor 는 SecurityMetadataSource 로 DelegatingMethodSecurityMetadataSource 이기 때문이다.
  • 나는 SecurityMetadataSource 로 MapBasedMethodSecurityMetadataSource 로 설정하고 싶기 때문이다.
  • CustomMethodSecurityInterceptor 로 advice(인가처리) 를 처리할 것이다.





Config 파일 에서 CustomMethodSecurityInterceptor 빈으로 등록

    @Bean
    public CustomMethodSecurityInterceptor customMethodSecurityInterceptor(MapBasedMethodSecurityMetadataSource methodSecurityMetadataSource) {
        CustomMethodSecurityInterceptor customMethodSecurityInterceptor =  new CustomMethodSecurityInterceptor();
        customMethodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
        customMethodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
        customMethodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
        RunAsManager runAsManager = runAsManager();
        if (runAsManager != null) {
            customMethodSecurityInterceptor.setRunAsManager(runAsManager);
        }

        return customMethodSecurityInterceptor;
    }
  • 위의 코드에서 MapBasedMethodSecurityMetadataSource 객체를 SecurityMetadataSource 로 넘겨주는 것을 확인할 수 있다.




REFERENCES

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

PasswordEncoder  (0) 2022.03.24
JWT  (0) 2022.02.28
Method 시큐리티 프로세스 커스텀  (0) 2022.02.28
URL 시큐리티 프로세스 커스텀  (0) 2022.02.28
권한 계층 적용하기  (0) 2022.02.28