개요
- 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 |