1. 개요
페이지별 즉 화면 단위별 인가처리를 하는 것이 아니라 메소드 단위로 인가처리를 적용할 수 있는 방법이다. 예를 들어서 UserServiceImpl에 아래 처럼 order 메서드를 추가할 수 있다. 그리고 @Secured라는 어노테이션을 적용해서 해당 method에 접근할 때는 "ROLE_MANAGER" 권한이 있는지 검사하도록 할 수 있는 것이다. 어떠 방식으로 동작하는 지 그 구조와 원리에 대해서 학습해본다.
@Override
@Secured("ROLE_MANAGER")
public void order() {
System.out.println("order");
}
2. 동작 방식
서버를 실행할 때 초기화가 진행된다. 초기화 때 보안 어노테이션이 있는 빈의 프록시 객체를 만들고, 여기에 인가처리를 하는 Advice를 만들어 놓는다. 그리고 이 프록시 객체에서 Advice를 동작시켜 인가 처리를 하고, 통과 시에 실제 빈 메서드를 호출하는 방식이다.
초기화
1. 전체 빈을 검사하면서 보안 설정 메서드 탐색(보안 어노테이션이 있는 메서드)
2. 해당 빈의 프록시 객체 생성
3. 보안 메서드에 인가처리를 하는 Advice 등록
4. 프록시 빈 객체를 참조하여 인가처리 진행
진행과정
1. 프록시 객체를 통해서 메서드 호출
2. Advice를 작동시켜 인가처리
3. 권한 심사 통과 시, 실제 빈 메서드 호출
3. 주요 아키텍쳐
서버를 시작했을 때 초기화 과정은 아래처럼 도식화 할 수 있다. MethodSecurityMetadataSourceAdvisor가 주체이며 이 객체가 보안 설정이 되어있는 메서드를 탐색하고 어드바이스 등록을 한다. Spring AOP 개념이 조금 들어가는데, 보안설정을 검사하는 기능을 부가적인 기능인 Advice라고 보고, 이 기능을 탐색하는 요소들로 구성된다.
초기화
MethodSecurityMetadataSource에서 보안 설정된(보안 어노테이션이 적용된) 메서드를 분석 후, DefaultAdvisorAutoProxyCreator에 의해 Proxy객체가 생성된다. MethodSecurityMetadataSourceAdvisor와 DefaultAdvisorAutoProxyCreator가 관계를 가진 것처럼 강의에서 설명하는데, BeanFactoryAware를 공통으로 implemets할 뿐 정확한 관계 및 원리를 이해하기는 어려웠다. Spring 공식 문서를 봐도 과연 이것이 어떻게 작동하는지 정확한 판단은 어렵다. 다만, Proxy 객체를 생성하기 위해서는 Factory를 통해서 따로 설정이 필요한데, DefaultAdvisorAutoProxyCreator는 따로 설정이 필요 없이 프록시 객체를 생성하는 구현체 정도로만 이해했다.
진행과정
실제 객체가 아니라 프록시 객체에 의해서 메서드가 호출된다. 그리고 앞선 초기화 과정에서 Advice가 MethodSecurityInterceptor에 등록이 됬다면 Advice에 등록된 권한을 검사 후, 승인된 경우 실제 객체의 메서드에 접근하여 실제 객체의 메서드를 호출한다.
4. 실습
메서드 방식 보안 설정
UserServiceImpl의 order() 메서드에 @Secured 어노테이션을 추가하여 테스트 해본다.
@Override
@Secured("ROLE_MANAGER")
public void order() {
System.out.println("order");
}
userController에 order() 메서드를 불러오는 부분을 작성해준다. /mypage에 접근 시 보안 설정 어노테이션인 @Secured가 작동하도록 해준다.
@GetMapping("/mypage")
public String myPage() {
userService.order();
return "/mypage";
}
SecurityConfig에 @EnableGlobalMethodSecurity 어노테이션을 추가해주어야 메서드 방식의 보안 설정 방식이 작동하게 된다. 속성값으로 여러 값을 줄 수 있는데, 자세한 내용은 나중에 알아보고, 우선 securedEnable = true로 설정해준다.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true) //메소드 보안 활성화
@Slf4j
@RequiredArgsConstructor
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...중략...
디버깅
실제 메서드 방식 보안 설정의 주체가 되는 MethodSecurityMetadataSourceAdvisor 파일에 break point를 걸고, 서버를 시작해본다.
public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private transient MethodSecurityMetadataSource attributeSource;
private transient MethodInterceptor interceptor;
private final Pointcut pointcut = new MethodSecurityMetadataSourceAdvisor.MethodSecurityMetadataSourcePointcut();
private BeanFactory beanFactory;
private final String adviceBeanName;
private final String metadataSourceBeanName;
private transient volatile Object adviceMonitor = new Object();
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;
}
디버깅 콘솔에서 MethodSecurityMetadataSource인 attributeSource 부분이 DelegatingMethodSecurityMetadataSource를 참조하고 있음을 알 수 있다. 여기에 실제 보안 설정된 어노테이션을 parse하는 객체들이 담긴다. 일단 @Secured 어노테이션만 작성했기 때문에 methodSecurityMetadataSources에 1개만 담겨있는 것을 확인할 수 있다.
그리고 해당 클래스의 아래쪽에 matches 메서드를 확인할 수 있는데, 이 메서드가 모든 클래스들을 탐색하며 보안 설정을 가진 Bean이 있는지 검사하는 역할을 한다.
public boolean matches(Method m, Class targetClass) {
Collection attributes = MethodSecurityMetadataSourceAdvisor.this.attributeSource.getAttributes(m, targetClass);
return attributes != null && !attributes.isEmpty();
}
추가로, 초기화 과정에서 DelegatingMethodSecurityMetadataSource 부분에서 attributeCache에 method 방식의 보안 정보를 map 형태로 저장한다는 것도 기억해두자. 참고로 this.attribute 내부를 보면 수많은 메서드와 프록시 객체들이 caching되어 저장되는 것도 볼 수 있다.
좀 더 상세한 초기화 과정을 살펴볼 수 있는데, 다소 복잡하고 이해가 어렵다. 향후 Spring Security에 더 적응하고 깊은 단계까지 공부하게 된다면 파헤쳐 봐야겠다. 시간이 너무 오래 걸린다.
로그인 후 보안 메서드 접근
user 아이디로 로그인하여 /mypage에 접근하면 아래 사진의 break point에서 userService 객체가 CGLIB 프록시 객체로 대체되어있는 것을 확인할 수 있다.
그리고 원래는 해당 컨트롤러에서 바로 service의 order 메서드에 접근해야겠지만, 프록시 객체이고 보안 설정이 되어 있으므로 MethodSecurityInterceptor에서 권한 검사를 하게 된다. 코드에서 볼 수 있듯이, invoke 메서드에서 super.을 통해 부모 클래스인 AbstractSecurityInterceptor의 메서드를 실행하면서 인가 처리를 한다.
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {
private MethodSecurityMetadataSource securityMetadataSource;
public MethodSecurityInterceptor() {
}
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);
}
...중략...
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%E
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티]25. Method 방식 : Map 기반 DB 연동 (0) | 2022.03.01 |
---|---|
[스프링 시큐리티]24. Method 방식 : 어노테이션 API 이해 (0) | 2022.02.28 |
[스프링 시큐리티]22. 아이피 접속 제한 ; AccessDecisionVoter 추가 (0) | 2022.02.25 |
[스프링 시큐리티]21. 계층 권한 (0) | 2022.02.25 |
[스프링 시큐리티]20. 실시간 권한 업데이트, 허용필터 (0) | 2022.02.20 |