인프런 커뮤니티 질문&답변

Truestar님의 프로필 이미지
Truestar

작성한 질문수

스프링 시큐리티

6) AOP Method 기반 DB 연동 - MapBasedSecurityMetadataSource (3)

런타임 중, 메소드 인가 맵 등록시, 서비스 Proxy 가 만들어지지 않는 이유의 질문입니다.

해결된 질문

작성

·

332

0

좋은강의 감사드립니다.

약간 다른 시도를 해보고 잘 안풀리게되어 질문을 남기게 되었어요.
메소드 권한 부여는 반드시 MapBasedMethodSecurityMetadataSource 생성자를 통해서만 등록이 가능할까요? 
초기화가 끝난 이후 런타임에서 addSecureMethod(string, configAttrList) 를 통해 등록하면 프록시 생성이 안되는것 같더라구요.. 그래서 아래와 같은 시도가 있었습니다.

 

저는 MapBasedMethodSecurityMetadataSourceextends 하여 
CustomMapBasedMethodSecurityMetadataSource 를 만들어, 생성이 된 이후, 메소드 리소스맵 등록을  super.addSecureMethod() 메서드로  하려는 시도를 했습니다. 


이후 @EventListener(ContextRefreshedEvent.class) 이벤트 핸들러를 MethodSecurityConfig 에 작성하고,

이벤트 발생 시점에 App컨택스트로 부터 CustomMapBasedMethodSecurityMetadataSource 를 가져와 reload() 를 호출하여 Map 을 통해 메서드 정보 등록이 되도록 구성했습니다

문제는
서버 기동 및 컨트롤러 호출 후, Method Resource 가 등록 과정에 서비스 프록시 가 생성되지 않아 서비스 메서드 가 그대로 호출이 되었는데요, 

Debug  확인 결과 클래스 명 메소드명 Map 파싱은 문제가 없었습니다.

아래는 서버 기동 후,
커스텀 메소드 메타데이터 소스 를 메모리에서 조회 결과입니다.

 

위의 과정으로
반드시 생성자를 통해 Map 을 전달 해야만 Proxy 생성이 되는것으로 판단되었습니다
이벤트 리스너를 통해 methodMap 등록을 지연하게 되면 필터링 처리가 안되는 이유가 궁금한데요..
이런 부분에 대해 조언을 구합니다.

 

아래는 작성한 Method..Config 와 Method...Source 입니다

MethodSecurityConfig

public class MethodSecurityConfig {
...

/**
* DB 초기화 직후, METHOD 인가정보 등록
*/
@EventListener(ContextRefreshedEvent.class)
@Transactional
public void onContextRefreshed(ContextRefreshedEvent event) {
ApplicationContext ctx = event.getApplicationContext();

var customMapBasedMethodSecurityMetadataSource =
ctx.getBean(CustomMapBasedMethodSecurityMetadataSource.class);

customMapBasedMethodSecurityMetadataSource.reload();
}

...
}

 

CustomMapBasedMethodSecurityMetadataSource

public class CustomMapBasedMethodSecurityMetadataSource
extends MapBasedMethodSecurityMetadataSource {
private final MethodResourceMapFactoryBean methodResourceMapFactoryBean;

public CustomMapBasedMethodSecurityMetadataSource(MethodResourceMapFactoryBean methodResourceMapFactoryBean) {
/* 생성자를 통해 methodMap 전달시 작동 */
// super(Map.of(
// "io.security.corespringsecurity.aopsecurity_test.AopMethodAuthTestService.methodSecured",
// List.of(new SecurityConfig("ROLE_USER"))
// ));
this.methodResourceMapFactoryBean = methodResourceMapFactoryBean;
}

/**
* DB 데이터 초기와 직전 로딩 이슈로, DB 초기화 이후 값을 가져오기위한 리로딩 메서드
*/
public void reload() {

LinkedHashMap<String, List<ConfigAttribute>> resourceMap = methodResourceMapFactoryBean.getObject();

for (Map.Entry<String, List<ConfigAttribute>> resourceEntry : resourceMap.entrySet()) {
String fullPackageClassMethodName = resourceEntry.getKey();
List<ConfigAttribute> configAttributes = resourceEntry.getValue();

addSecureMethod(fullPackageClassMethodName, configAttributes);
}
}

/**
* 보안 메서드에 대한 설정을 추가합니다. 메서드 이름은 여러 메서드를 등록하기 위해 `*` 로 끝나거나 시작할 수 있습니다.<br />
* 풀패키지 클래스명 + 메서드명 파싱 및 S.Security 에 메서드 정보 추가 <br />
* Key: 풀패키지 클래스명 + 메서드명(ex: "a.b.Class.*method or method*") <br />
* Value: ConfigAttribute List <br />
* 참고: super 클래스 private addSecureMethod(name, attr) 메소드 복제
*/
private void addSecureMethod(String name, List<ConfigAttribute> attr) {
int lastDotIndex = name.lastIndexOf(".");
Assert.isTrue(lastDotIndex != -1, () -> "'" + name + "' is not a valid method name: format is FQN.methodName");
String methodName = name.substring(lastDotIndex + 1);
Assert.hasText(methodName, () -> "Method not found for '" + name + "'");
String typeName = name.substring(0, lastDotIndex);
Class<?> type = ClassUtils.resolveClassName(typeName, ClassUtils.getDefaultClassLoader());

super.addSecureMethod(type, methodName, attr);
}

}

 

읽어주셔서 감사합니다.

답변 1

3

정수원님의 프로필 이미지
정수원
지식공유자

네 음...

이 부분은 조금 복잡한데요..

전반적으로 AOP 와 관련된 내용들이 주를 이루고 있고 스프링 시큐리티가 초기화 되는 과정속에서 여러 단계들을 거치면서 프록시 객체 기술을 통한 권한 설정을 하고 있습니다.

또한 초기화 시점에서 생성되는 프록시 객체는 스프링이 자동으로 해 주고 있습니다.
그 기준이 되는 것은 MapBasedMethodSecurityMetadataSource 에 설정한 클래스와 메소드를 통해 프록시 대상이 되는지 여부를 검사하게 되고 검사가 통과하게 되면 해당 클래스는 프록시 객체가 되고 해당 메소드는 권한을 체크하는 MethodInterceptor 대상으로 등록되게 됩니다.

그렇기 때문에 MethodSecurityConfig  설정클래스에서

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
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;
}

위의 구문에서 

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

이 부분이 Override 되고 있는데 이 규칙은 임의로 변경할 수 있는 부분이 아닙니다.

이 구문으로 인해 MapBasedMethodSecurityMetadataSource 를 통해 AOP 대상이 되는 클래스와 메소드를 찾게 됩니다. 

그리고 내부적으로 CglibAopProxy 클래스에 의해 프록시 객체가 생성되게 됩니다.

그래서 

@EventListener(ContextRefreshedEvent.class)
@Transactional
public void onContextRefreshed(ContextRefreshedEvent event) {
ApplicationContext ctx = event.getApplicationContext();

var customMapBasedMethodSecurityMetadataSource =
ctx.getBean(CustomMapBasedMethodSecurityMetadataSource.class);

customMapBasedMethodSecurityMetadataSource.reload();
}

위의 구문은 스프링 시큐리티가 메소드 권한을 위한 내부 프로세스의 과정과 흐름에서 벗어나 있기 때문에 스프링 시큐리티가 해당 클래스와 메소드를 프록시로 생성해야 하는지에 대한 인식을 하지 못합니다.

만약 CustomMapBasedMethodSecurityMetadataSource 를 꼭 사용해야 한다면

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

구문을

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

와 같은 방법으로 변경해서 사용해 볼 수는 있을 것 같은데 정확하게 동작할지는 잘 모르겠습니다.

 

참고하실 클래스는 다음과 같습니다.

MethodSecurityMetadataSourceAdvisor 클래스는 메소드 권한을 AOP 방식으로 적용하기 위한 내용을 담고 있습니다.

PointCut 과 MethodInterceptor 를 동시에 가지고 있는 Advisor 입니다.

public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor
implements BeanFactoryAware {
// ~ Instance fields
// ================================================================================================

private transient MethodSecurityMetadataSource attributeSource;
private transient MethodInterceptor interceptor;
private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut();
private BeanFactory beanFactory;
private final String adviceBeanName;
private final String metadataSourceBeanName;
private transient volatile Object adviceMonitor = new Object();

// ~ Constructors
// ===================================================================================================

/**
* Alternative constructor for situations where we want the advisor decoupled from the
* advice. Instead the advice bean name should be set. This prevents eager
* instantiation of the interceptor (and hence the AuthenticationManager). See
* SEC-773, for example. The metadataSourceBeanName is used rather than a direct
* reference to support serialization via a bean factory lookup.
*
* @param adviceBeanName name of the MethodSecurityInterceptor bean
* @param attributeSource the SecurityMetadataSource (should be the same as the one
* used on the interceptor)
* @param attributeSourceBeanName the bean name of the attributeSource (required for
* serialization)
*/
public MethodSecurityMetadataSourceAdvisor(String adviceBeanName,
MethodSecurityMetadataSource attributeSource, String attributeSourceBeanName) {
Assert.notNull(adviceBeanName, "The adviceBeanName cannot be null");
Assert.notNull(attributeSource, "The attributeSource cannot be null");
Assert.notNull(attributeSourceBeanName,
"The attributeSourceBeanName cannot be null");

this.adviceBeanName = adviceBeanName;
this.attributeSource = attributeSource;
this.metadataSourceBeanName = attributeSourceBeanName;
}

// ~ Methods
// ========================================================================================================

public Pointcut getPointcut() {
return pointcut;
}

public Advice getAdvice() {
synchronized (this.adviceMonitor) {
if (interceptor == null) {
Assert.notNull(adviceBeanName,
"'adviceBeanName' must be set for use with bean factory lookup.");
Assert.state(beanFactory != null,
"BeanFactory must be set to resolve 'adviceBeanName'");
interceptor = beanFactory.getBean(this.adviceBeanName,
MethodInterceptor.class);
}
return interceptor;
}
}

public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

private void readObject(ObjectInputStream ois) throws IOException,
ClassNotFoundException {
ois.defaultReadObject();
adviceMonitor = new Object();
attributeSource = beanFactory.getBean(metadataSourceBeanName,
MethodSecurityMetadataSource.class);
}

// ~ Inner Classes
// ==================================================================================================

class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut
implements Serializable {
@SuppressWarnings("unchecked")
public boolean matches(Method m, Class targetClass) {
Collection attributes = attributeSource.getAttributes(m, targetClass);
return attributes != null && !attributes.isEmpty();
}
}
}

보시면

class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut
implements Serializable {
@SuppressWarnings("unchecked")
public boolean matches(Method m, Class targetClass) {
Collection attributes = attributeSource.getAttributes(m, targetClass);
return attributes != null && !attributes.isEmpty();
}
}

위 구문 실행 후 MapBasedMethodSecurityMetadataSource 에 설정한 클래스와 메소드를 찾게 되면 해당 클래스는 프록시 객체의 대상으로 간주되고 메소드에는 MethodInterceptor 가 작용되도록 설정됩니다. 

attributes != null && !attributes.isEmpty();
구문인데 이것은 우리가 DB 에서 설정한 메소드명 리소스에 해당하는 권한값이 존재할 경우 참을 반환하게 됩니다.
attributes 은 List<ConfigAttribute> 라고 볼 수 있습니다

위 구문에서 참을 리턴하게 되면 해당 메소드에는 다음과 같이 메소드 권한을 위한 Advice 즉 MethodInterceptor 가 적용이 됩니다. 다음 구문입니다.

public Advice getAdvice() {
synchronized (this.adviceMonitor) {
if (interceptor == null) {
Assert.notNull(adviceBeanName,
"'adviceBeanName' must be set for use with bean factory lookup.");
Assert.state(beanFactory != null,
"BeanFactory must be set to resolve 'adviceBeanName'");
interceptor = beanFactory.getBean(this.adviceBeanName,
MethodInterceptor.class);
}
return interceptor;
}
}

결론적으로 메소드 권한이 대상이 되는 클래스의 프록시 생성 과정은 스프링 시큐리티의 초기와 과정속에서 여러 클래스들의 참조를 통해 이루어지기 때문에 커스텀하게 임의로 설정할 수 있는 성격은 아닙니다.
조금이나마 도움이 되셨길 바랍니다.


만약 런타임에서 실시간 적으로 메소드 권한 설정이 필요하다면 아래 소스를 참고해 보셔도 됩니다.
제가 강의에서 번외로 설명한 부분인데 코드가 약간 업그레이드 된 버전입니다.
스프링 시큐리티에서 사용하고 있는 MethodSecurityMetadataSourceAdvisor 를 참조해서 만든 것인데 추가 및 삭제가 런타임때 실시간 적으로 정상 동작하고 있습니다.
참고용으로 하시면 될 것 같습니다.

@Component
public class MethodSecurityService {

private MapBasedMethodSecurityMetadataSource mapBasedMethodSecurityMetadataSource;
private AbstractApplicationContext applicationContext;
private CustomMethodSecurityInterceptor methodSecurityInterceptor;
private DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
private MethodSecurityMetadataSourceAdvisor methodSecurityMetadataSourceAdvisor;

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

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

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

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

ProxyFactory proxyFactory = advisedMap.get(beanName);
Object target = targetMap.get(beanName);
Object proxy = proxyMap.get(beanName);
Object advised = applicationContext.getBean(beanName);

if(proxyFactory == null && !(advised instanceof Advised)) {

proxyFactory = new ProxyFactory();

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

} else {
proxyFactory.setTarget(target);
}

defaultPointcutAdvisor.setAdvice(methodSecurityInterceptor);
proxyFactory.addAdvisor(defaultPointcutAdvisor);
advisedMap.put(beanName, proxyFactory);

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);
}

}else if(proxyFactory == null && (advised instanceof Advised)){
defaultPointcutAdvisor.setAdvice(methodSecurityInterceptor);
((Advised) advised).addAdvisor(defaultPointcutAdvisor);

}else if(proxyFactory != null){
proxyFactory.addAdvisor(defaultPointcutAdvisor);
}
}

public void removeMethodSecured(String className) throws Exception{

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

ProxyFactory proxyFactory = advisedMap.get(beanName);

if(proxyFactory != null){
proxyFactory.removeAdvisor(defaultPointcutAdvisor);

}else{
Object bean = applicationContext.getBean(beanName);
Advisor[] advisors = ((Advised) bean).getAdvisors();

for(Advisor advisor : advisors) {
if (advisor instanceof MethodSecurityMetadataSourceAdvisor) {
((Advised) bean).removeAdvisor(methodSecurityMetadataSourceAdvisor);

} else if (advisor instanceof DefaultPointcutAdvisor) {
((Advised) bean).removeAdvisor(defaultPointcutAdvisor);
}
Object singletonTarget = AopProxyUtils.getSingletonTarget(bean);
targetMap.put(beanName, singletonTarget);
}
}
}

private Class<?> getType(String className, int lastDotIndex) {
String typeName = className.substring(0, lastDotIndex);
return ClassUtils.resolveClassName(typeName, ClassUtils.getDefaultClassLoader());
}
}

그리고 질문하신 내용과 관련해서 전반적인 AOP 의 흐름과 스프링 시큐리티가 내부적으로 어떤 단계로 메소드 권한을 위한 프록시 객체를 활용하는지에 대한 내용을 설명드리고 싶지만 부득이 현 지면에 담기에는 내용이 많고 복잡도가 있고 스프링 시큐리티에 대한 주제를 벗어나기 때문에 상세한 내용을 드리기가 어려운점 양해 부탁드립니다.

Truestar님의 프로필 이미지
Truestar
질문자

늦은시간에 긴 답변 감사드려요. 알려주신 내용을 바탕으로 응용에 단계를 밟아보겠습니다. Aop에 관한 내용도 함께 봐야겠네요..

깊은 배려에 감사드립니다.

Truestar님의 프로필 이미지
Truestar

작성한 질문수

질문하기