Method 방식 - 개요
서비스 계층의 인가처리 방식
1. 화면 , 메뉴 단위가 아닌 기능 단위로 인가처리
2. 메소드 처리 전.후 로 보안 검사 수행하여 인가처리
AOP 기반으로 동작
프록시와 어드바이스로 메소드 인가처리 수행
보안 설정 방식
어노테이션 권한 설정 방식 = @PreAuthorize("hasRole('USER')"), @PostAuthorize("hasRole('USER')"), @Secured("ROLE_USER")
맵 기반 권한 설정 방식 = 맵 기반 방식으로 외부와 연동하여 메소드 보안 설정 구현
어노테이션 권한 설정 - @PreAuthorize , @PostAuthorize , @Secured , @RolesAllowed
@PreAuthorize, @PostAuthorize
Spel 지원 = @PreAuthorize("hasRole('ROLE_USER') and (#account.username == principal.username)")
PrePostAnnotationSecurityMetadataSource 가 담당
@Secured, @RolesAllowed
spel 미지원 = @Secured("ROLE_USER")
SecuredAnnotationSecurityMetadataSouce, Jsr250MethodSecurityMetadataSource 가 담당
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
쓸 method들을 true로 바꿔야한다. 기본값은 다 false이다.
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
List<MethodSecurityMetadataSource> sources = new ArrayList<>();
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
getExpressionHandler());
MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
if (customMethodSecurityMetadataSource != null) {
sources.add(customMethodSecurityMetadataSource);
}
boolean hasCustom = customMethodSecurityMetadataSource != null;
boolean isPrePostEnabled = prePostEnabled();
boolean isSecuredEnabled = securedEnabled();
boolean isJsr250Enabled = jsr250Enabled();
Assert.state(isPrePostEnabled || isSecuredEnabled || isJsr250Enabled || hasCustom,
"In the composition of all global method configuration, "
+ "no annotation support was actually activated");
if (isPrePostEnabled) {
sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
}
if (isSecuredEnabled) {
sources.add(new SecuredAnnotationSecurityMetadataSource());
}
if (isJsr250Enabled) {
GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context
.getBean(Jsr250MethodSecurityMetadataSource.class);
if (grantedAuthorityDefaults != null) {
jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
sources.add(jsr250MethodSecurityMetadataSource);
}
return new DelegatingMethodSecurityMetadataSource(sources);
}
true 된 것들만 추가하여 bean으로 생성해준다.
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
DefaultCacheKey cacheKey = new DefaultCacheKey(method, targetClass);
synchronized (this.attributeCache) {
Collection<ConfigAttribute> cached = this.attributeCache.get(cacheKey);
// Check for canonical value indicating there is no config attribute,
if (cached != null) {
return cached;
}
// No cached value, so query the sources to find a result
Collection<ConfigAttribute> attributes = null;
for (MethodSecurityMetadataSource s : this.methodSecurityMetadataSources) {
attributes = s.getAttributes(method, targetClass);
if (attributes != null && !attributes.isEmpty()) {
break;
}
}
// Put it in the cache.
if (attributes == null || attributes.isEmpty()) {
this.attributeCache.put(cacheKey, NULL_CONFIG_ATTRIBUTE);
return NULL_CONFIG_ATTRIBUTE;
}
this.logger.debug(LogMessage.format("Caching method [%s] with attributes %s", cacheKey, attributes));
this.attributeCache.put(cacheKey, attributes);
return attributes;
}
}
DelegatingMethodSecurityMetadataSource 의 getAttribute 메소드에서 enable된 annotation에 적혀 있는 정보들을 다 읽어온다.
읽어온 정보를 맵에 저장을 하게 된다. -> 메소드 권한 심사를 위한 초기화가 완료된다.
@GetMapping("/preAuthorize")
@PreAuthorize("hasRole('ROLE_USER') and #accountDto.username == #principal.username")
public String preAuthorize(AccountDto accountDto, Model model, Principal principal) {
model.addAttribute("method", "Success PreAuthorize");
return "aop/method";
}
url 검사는 FilterSecurityInterceptor에서 통과하게 된다. 현재 아무 권한 없이 들어갈 수 있게 되어있으니( 로그인 안해도)
method security가 enabled 되어있으니 filter들을 다 통과하고 mvc 쪽으로가서 dispatcher servlet이 해당 url로 이동하게 된다.
하지만 aop의 프록시 객체를 거치기 위해서 MethodSecurityInterceptor (AbstractSecurityInterceptor를 구현하고 있음)에서 초기화때 map에 저장한 권한정보를 기반으로 @PreAuthorize관련 인가 처리를 한다.
AOP Method 기반 DB 연동 - 주요 아키텍처 이해
첫번쨰 그림처럼 우선 보안이 설정된 메소드가 있는 지 탐색한다.
MethodSecurityMetadataSourcePointcut 에서 class , method 정보를 넘겨주고(DefaultAdvisorAutoProxyCreator로 부터 계속 넘어온다 같은것이, servlet의 request,response처럼)
MethodSecurityMetadataSource가 해당 클래스 및 메소드에 보안설정이 있는지 탐색하고 들고 있는다.
Pointcut이니까 매칭된 class,method정보를 캐싱해놓는다.
advice는 MethodSecurityInterceptor 가 담당하게 되고 Proxy를 생성하여 adviosr를 필드로 들고있게 된다. 밑에 더 자세히
초기화가 끝난 이후에 과정이다.
OrderService Proxy 가 요청을 먼저 처리하고 Advice가 등록되어있으면 인가처리를 하여 해당 메소드를 호출할지 말지 결정하게 된다.
원본 객체가 아니라 프록시 객체에 진입해서 인가 과정 (FilterInterceptorSecurity(url관련) 처럼 MethodSecurityInterceptor가 인가 과정을 처리한다)
AOP Method 기반 DB 연동 - MapBasedSecurityMetadataSouce
어떤 url 이 어떤 Authoriy 를 필요한지와 어떤 class의 method가 어떤 Authority가 필요한지를 db에서 받아오고 비교하게 된다.
MapBasedMethodSecurityMetadataSource는 기본적인 구현체로서 db로부터 자원과 권한정보를 매핑한 데이터를 초기화 과정을 거쳐서 메소드 방식의 인가처리가 이루어질 수 있도록 해준다.
admin 메소드는 ROLE_ADMIN 권한이 있어야 한다고 가정한다. 그러면 프록시 객체에서 인가처리를 MethodSecurityInterceptor를 통해서 먼저 진행한다.
초기화 과정 이후니까 MethodMap에 권한정보가 mapping이 끝난상태이니 비교만 하면 된다.
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
return new MapBasedMethodSecurityMetadataSource();
}
}
메소드 security설정은 따로 클래스를 분리하였다.
db에는 패키지+클래스+메소드 까지 담겨있지만 parsing을 한 이후에 methodMap에 들어가게 된다.
기존 url 방식하고 parse만 차이가 있다.
main-> refreshContext -> AbstractAutowireCapableBeanFactory#initializeBean-> AbstractAutoProxyCreator#wrapIfNecessary ->
proxy를 만들어야하는가 하는 과정에서 다음과 같은 과정이 일어나게 된다.
MethodSecurityMetadataSourceAdvisor 안에 MethodSecurityMetadataSourcePointcut#matches에서 여러 빈들을 검사하면서 proxy를 만들어야하는 클래스및 메소드를 MapBaseMethodSecurityMetadataSource#methodMap을 기준으로 찾아서
DelegatingMethodSecurityMetadataSource#attributeCache에 저장하게 된다.
이후 MethodSecurityInterceptor를 advisor에 adivce로 등록해주고 위에서 쓰인 MethodSecurityMetadataSourcePointcut 가 pointcut으로 등록이 된다. 위에서 attributeCache에 저장되었으니 매번 pointcut인지 확인할떄 캐싱된 값을 돌려주게 된다.
advisor = advice + pointcut으로 완성이 되었으니 proxy 를 생성하면서 안에 인자로 넣어주게 된다.
이후에 런타임에 요청이 들어오면 proxy에서 MethodSecurityInterceptor 에서 인가처리를 하게 된다.
AOP Method 기반 DB 연동 - ProtectPointcutPostProcessor
Method 방식 - ProtectPointcutPostProcessor
메소드 방식의 인가처리를 위한 자원 및 권한정보 설정 시 자원에 포인트 컷 표현식을 사용할 수 있도록 지원하는 클래스
빈 후처리기로서 스프링 초기화 과정에서 빈 들을 검사하여 빈이 가진 메소드 중에서 포인트컷 표현식과 matching 되는 클래스, 메소드 , 권한 정보를 MapBasedMethodSecurityMetadataSource에 전달하여 인가처리가 되도록 제공되는 클래스
DB저장 방식
Method 방식 = io.security.service.OrderService.order = ROLE_USER
Pointcut 방식 = execution(* io.security.service.*Service.*(..)): ROLE_USER
설정 클래스에서 빈 생성시 접근제한자가 package범위로 되어있기 때문에 리플렉션을 이용해 생성한다.
ProtectPointcutPostProcessor는 빈 후처리기로 초기화 이후 위에처럼 proxy 만들기 전에 확인하는 과정에서 db로 부터 얻은 권한 자원 정보를 methodMap에 들고 있는다.
윗부분은 동일한다.
db에서 pointcut 표현식을 그대로 읽어오고
ProtectPointcutPostProcessor가 프록시로 만들어야할 Class, Method 를 찾는 과정에서 매칭되는것을 찾아준다.
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (this.processedBeans.contains(beanName)) {
// We already have the metadata for this bean
return bean;
}
synchronized (this.processedBeans) {
// check again synchronized this time
if (this.processedBeans.contains(beanName)) {
return bean;
}
// Obtain methods for the present bean
Method[] methods = getBeanMethods(bean);
// Check to see if any of those methods are compatible with our pointcut
// expressions
for (Method method : methods) {
for (PointcutExpression expression : this.pointCutExpressions) {
// Try for the bean class directly
if (attemptMatch(bean.getClass(), method, expression, beanName)) {
// We've found the first expression that matches this method, so
// move onto the next method now
break; // the "while" loop, not the "for" loop
}
}
}
this.processedBeans.add(beanName);
}
return bean;
}
db로 붙어 받아온 this.pointCutExpressions 와 전달받은 클래스,메소드정보를 비교해서
attemptMatch안에서 일치하는 것들은 MapBasedMethodSecurityMetadataSource#methodMap에 저장을 하게 된다.
런타임에 권한처리를 할때 methodMap을 참조하게 된다.
구현이 굉장히 이상하다 강사분도 해당 방법이 맞는지 모르겠다 하심 추후 보완해야함
@Bean
public ProtectPointcutPostProcessor protectPointcutPostProcessor() {
ProtectPointcutPostProcessor protectPointcutPostProcessor = new ProtectPointcutPostProcessor(mapBasedMethodSecurityMetadataSource());
protectPointcutPostProcessor.setPointcutMap(pointcutResourceMapFactoryBean().getObject());
return protectPointcutPostProcessor;
}
기존 spring security에 들어있는 ProtectPointcutPostProcessor를 복붙하여
/**
* 설정클래스에서 람다 형식으로 선언된 빈이 존재할 경우 에러가 발생하여 스프링 빈과 동일한 클래스를 생성하여 약간 수정함
* 아직 AspectJ 라이브러리에서 Fix 하지 못한 것으로 판단되지만 다른 원인이 존재하는지 계속 살펴보도록 함
*/
private boolean attemptMatch(Class<?> targetClass, Method method, PointcutExpression expression, String beanName) {
boolean matches;
try {
matches = expression.matchesMethodExecution(method).alwaysMatches();
if (matches) {
List<ConfigAttribute> attr = pointcutMap.get(expression.getPointcutExpression());
if (log.isDebugEnabled()) {
log.debug("AspectJ pointcut expression '"
+ expression.getPointcutExpression() + "' matches target class '"
+ targetClass.getName() + "' (bean ID '" + beanName
+ "') for method '" + method
+ "'; registering security configuration attribute '" + attr
+ "'");
}
mapBasedMethodSecurityMetadataSource.addSecureMethod(targetClass, method, attr);
}
return matches;
} catch (Exception e) {
matches = false;
}
return matches;
}
해당 메소드를 try catch 로 감싸서 에러가 발생하더라도 계속 진행할 수 있도록 Exception을 먹어버린다.
'WEB > Security' 카테고리의 다른 글
실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현 (0) | 2023.01.16 |
---|---|
실전프로젝트 - 인증 프로세스 Ajax 인증 구현 (0) | 2023.01.15 |
스프링 시큐리티 주요 아키텍처 이해 (0) | 2023.01.10 |
스프링 시큐리티 기본 API 및 Filter 이해 (안겹치는것만) (0) | 2023.01.08 |
OAuth 2.0 Client + Resource Server + Authorization Server 연동 (0) | 2022.12.28 |