개요
- 메소드에 실시간으로 보안처리를 하고 싶다.
- 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